From f026cd2e60ed812feba5c71f4720a0f80c35c5f5 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 11 Feb 2023 22:20:15 +0100 Subject: [PATCH 01/64] [mybmw] fix not working binding due to API update to make it work the code has been refactored and due to API changes some improvements could be made. These include: - (improvement) fingerprint generation: You can take a look at the README how to create a fingerprint more conveniently. - (change) changed channel: charge-info has been renamed to charge-remaining - (improvement) added channels: estimated-fuel-l-100km and estimated-fuel-mpg which calculates the estimated fuel consumption based on the range and remaining fuel liters - unfortunately such a calculation is not available for EVs as there is no information about the capacity of the battery. - (improvement) added channel last-fetched: the last-updated timestamp is showing by when the last update of the vehicle happened. As right now you can not see from the channels if a thing is offline due to connection issues, you can check now if last-fetched is more than 5 minutes ago to identify an issue - (fixed) remote command typos fixed Co-authored-by: Mark Herwege Fixes #14065 Also-by: Mark Herwege Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/.gitignore | 1 + bundles/org.openhab.binding.mybmw/README.md | 451 ++++---- bundles/org.openhab.binding.mybmw/pom.xml | 168 +++ ...ion.java => MyBMWBridgeConfiguration.java} | 5 +- .../mybmw/internal/MyBMWConstants.java | 249 ++-- .../internal/MyBMWVehicleConfiguration.java | 97 ++ .../mybmw/internal/VehicleConfiguration.java | 41 - .../console/MyBMWCommandExtension.java | 325 ++++++ .../internal/discovery/VehicleDiscovery.java | 297 ++--- .../internal/dto/auth/AuthQueryResponse.java | 14 + .../internal/dto/charge/ChargeProfile.java | 44 - .../internal/dto/charge/ChargeSession.java | 28 - .../internal/dto/charge/ChargeSessions.java | 27 - .../internal/dto/charge/ChargeStatistics.java | 26 - .../dto/charge/ChargeStatisticsContainer.java | 24 - .../internal/dto/charge/ChargingProfile.java | 80 ++ .../internal/dto/charge/ChargingSession.java | 96 ++ .../internal/dto/charge/ChargingSessions.java | 66 ++ ...er.java => ChargingSessionsContainer.java} | 6 +- .../internal/dto/charge/ChargingSettings.java | 56 +- .../dto/charge/ChargingStatistics.java | 110 ++ .../charge/ChargingStatisticsContainer.java | 77 ++ .../internal/dto/charge/ChargingWindow.java | 26 +- .../dto/charge/RemoteChargingCommands.java | 81 ++ .../mybmw/internal/dto/charge/Time.java | 25 +- .../internal/dto/network/NetworkError.java | 38 - .../mybmw/internal/dto/properties/CBS.java | 27 - .../mybmw/internal/dto/properties/CCM.java | 22 - .../dto/properties/ChargingState.java | 25 - .../internal/dto/properties/Coordinates.java | 23 - .../internal/dto/properties/Distance.java | 23 - .../mybmw/internal/dto/properties/Doors.java | 25 - .../internal/dto/properties/DoorsWindows.java | 26 - .../internal/dto/properties/FuelLevel.java | 23 - .../internal/dto/properties/Location.java | 24 - .../internal/dto/properties/Properties.java | 43 - .../mybmw/internal/dto/properties/Range.java | 23 - .../mybmw/internal/dto/properties/Tire.java | 22 - .../internal/dto/properties/TireStatus.java | 25 - .../mybmw/internal/dto/properties/Tires.java | 25 - .../internal/dto/properties/Windows.java | 25 - .../internal/dto/remote/ExecutionError.java | 2 +- .../dto/remote/ExecutionStatusContainer.java | 47 +- .../mybmw/internal/dto/status/CBSMessage.java | 27 - .../mybmw/internal/dto/status/CCMMessage.java | 30 - .../mybmw/internal/dto/status/DoorWindow.java | 25 - .../internal/dto/status/FuelIndicator.java | 41 - .../mybmw/internal/dto/status/Issues.java | 22 - .../mybmw/internal/dto/status/Mileage.java | 24 - .../mybmw/internal/dto/status/Status.java | 38 - .../dto/{properties => vehicle}/Address.java | 14 +- .../internal/dto/vehicle/Capabilities.java | 52 - .../dto/vehicle/CheckControlMessage.java | 75 ++ ...eService.java => ClimateControlState.java} | 26 +- .../internal/dto/vehicle/ClimateTimer.java | 67 ++ .../dto/vehicle/CombustionFuelLevel.java | 55 + .../internal/dto/vehicle/Coordinates.java | 45 + .../internal/dto/vehicle/DepartureTime.java | 45 + .../internal/dto/vehicle/DigitalKey.java | 55 + .../dto/vehicle/DriverPreferences.java | 36 + .../dto/vehicle/ElectricChargingState.java | 142 +++ .../internal/dto/vehicle/RequiredService.java | 73 ++ .../mybmw/internal/dto/vehicle/Vehicle.java | 49 +- .../dto/vehicle/VehicleAttributes.java | 148 +++ .../internal/dto/vehicle/VehicleBase.java | 47 + .../dto/vehicle/VehicleCapabilities.java | 233 ++++ .../dto/vehicle/VehicleDoorsState.java | 101 ++ .../internal/dto/vehicle/VehicleLocation.java | 54 + .../dto/vehicle/VehicleRoofState.java | 45 + .../internal/dto/vehicle/VehicleState.java | 231 ++++ .../dto/vehicle/VehicleStateContainer.java | 55 + .../dto/vehicle/VehicleTireState.java | 45 + .../dto/vehicle/VehicleTireStateDetails.java | 121 ++ ...VehicleTireStateDetailsClassification.java | 45 + .../dto/vehicle/VehicleTireStateStatus.java | 46 + .../dto/vehicle/VehicleTireStates.java | 64 ++ .../dto/vehicle/VehicleWindowsState.java | 82 ++ .../internal/handler/MyBMWBridgeHandler.java | 110 +- .../mybmw/internal/handler/MyBMWProxy.java | 515 --------- .../handler/RemoteServiceExecutor.java | 164 +++ .../handler/RemoteServiceHandler.java | 227 ---- .../internal/handler/ResponseCallback.java | 26 - .../handler/StringResponseCallback.java | 27 - .../handler/VehicleChannelHandler.java | 478 -------- .../internal/handler/VehicleHandler.java | 1023 +++++++++++++---- .../handler/auth/MyBMWTokenController.java | 381 ++++++ .../internal/handler/{ => auth}/Token.java | 2 +- .../backend/JsonStringDeserializer.java | 97 ++ .../handler/backend/MyBMWFileProxy.java | 200 ++++ .../handler/backend/MyBMWHttpProxy.java | 346 ++++++ .../internal/handler/backend/MyBMWProxy.java | 96 ++ .../handler/backend/NetworkException.java | 102 ++ .../backend/ResponseContentAnonymizer.java | 245 ++++ .../ExecutionState.java} | 20 +- .../internal/handler/enums/RemoteService.java | 64 ++ .../internal/handler/simulation/Injector.java | 43 - .../mybmw/internal/utils/BimmerConstants.java | 67 +- .../internal/utils/ChargeProfileWrapper.java | 303 ----- ...leUtils.java => ChargingProfileUtils.java} | 6 +- .../utils/ChargingProfileWrapper.java | 202 ++++ .../mybmw/internal/utils/Constants.java | 11 +- .../mybmw/internal/utils/Converter.java | 298 ++--- .../mybmw/internal/utils/HTTPConstants.java | 63 +- .../mybmw/internal/utils/ImageProperties.java | 3 +- .../utils/MyBMWConfigurationChecker.java | 34 + .../internal/utils/RemoteServiceUtils.java | 5 +- .../internal/utils/VehicleStatusUtils.java | 191 +-- .../resources/OH-INF/i18n/mybmw.properties | 217 ++-- .../resources/OH-INF/i18n/mybmw_de.properties | 394 ++++--- .../OH-INF/thing/conv-range-channel-group.xml | 2 + .../OH-INF/thing/ev-vehicle-status-group.xml | 4 +- .../thing/hybrid-range-channel-group.xml | 2 + .../OH-INF/thing/image-channel-types.xml | 7 +- .../OH-INF/thing/range-channel-types.xml | 10 + .../OH-INF/thing/tires-channel-groups.xml | 2 +- .../thing/vehicle-status-channel-types.xml | 20 +- .../OH-INF/thing/vehicle-status-group.xml | 2 +- .../internal/discovery/DiscoveryTest.java | 116 -- .../discovery/VehicleDiscoveryTest.java | 128 +++ .../mybmw/internal/dto/ChargeProfileTest.java | 54 - ...er.java => ChargingStatisticsWrapper.java} | 37 +- .../mybmw/internal/dto/StatusWrapper.java | 411 ++++--- .../internal/dto/VehiclePropertiesTest.java | 21 +- .../mybmw/internal/dto/VehicleStatusTest.java | 115 -- .../charge/ChargingStatisticsTest.java} | 91 +- .../internal/dto/vehicle/VehicleBaseTest.java | 83 ++ .../dto/vehicle/VehicleCapabilitiesTest.java | 47 + .../vehicle/VehicleStateContainerTest.java | 59 + .../internal/handler/ConfigurationTest.java | 47 - .../internal/handler/ErrorResponseTest.java | 78 -- .../internal/handler/SimulationTest.java | 33 - ...icleTests.java => VehicleHandlerTest.java} | 251 ++-- .../internal/handler/{ => auth}/AuthTest.java | 63 +- .../backend/JsonStringDeserializerTest.java | 136 +++ .../handler/backend/MyBMWHttpProxyTest.java | 222 ++++ .../handler/backend/MyBMWProxyBackendIT.java | 231 ++++ .../ResponseContentAnonymizerTest.java | 58 + .../mybmw/internal/util/FileReader.java | 41 +- .../mybmw/internal/utils/ConverterTest.java | 53 + .../utils/MyBMWConfigurationCheckerTest.java | 48 + .../resources/responses/530e/vehicles.json | 379 ------ .../responses/BEV/charging_sessions.json | 74 ++ .../responses/BEV/charging_statistics.json | 11 + .../responses/BEV/vehicles_base.json | 48 + .../responses/BEV/vehicles_state.json | 292 +++++ .../responses/BEV2/vehicles_state.json | 154 +++ .../responses/BEV3/charging_sessions.json | 50 + .../responses/BEV3/charging_statistics.json | 11 + .../responses/BEV3/vehicles_base.json | 48 + .../responses/BEV3/vehicles_state.json | 248 ++++ .../responses/BEV4/vehicles_base.json | 50 + .../responses/BEV4/vehicles_state.json | 311 +++++ .../responses/BEV5/vehicles_base.json | 50 + .../responses/BEV5/vehicles_state.json | 319 +++++ .../responses/F11/vehicles_v2_bmw_0.json | 279 ----- .../responses/F31/vehicles_v2_bmw_0.json | 281 ----- .../responses/F44/vehicles_v2_bmw_0.json | 251 ---- .../responses/F45/vehicles_v2_bmw_0.json | 301 ----- .../responses/F48/vehicles_v2_bmw_0.json | 278 ----- .../responses/G01/vehicles_v2_bmw_0.json | 429 ------- .../responses/G05/vehicles_v2_bmw_0.json | 401 ------- .../responses/G08/vehicles_v2_bmw_0.json | 401 ------- .../test/resources/responses/G21/340i.json | 401 ------- .../responses/G21/charging-sessions_0.json | 103 -- .../responses/G21/charging-statistics_0.json | 11 - .../resources/responses/G21/json_export.json | 831 ------------- .../responses/G21/vehicles_v2_bmw_0.json | 542 --------- .../responses/G30/charging-sessions_0.json | 63 - .../responses/G30/charging-statistics_0.json | 11 - .../responses/G30/vehicles_v2_bmw_0.json | 384 ------- .../I01_NOREX/charging-sessions_0.json | 23 - .../I01_NOREX/charging-statistics_0.json | 11 - .../I01_NOREX/vehicles_v2_bmw_0.json | 316 ----- .../responses/I01_REX/charge-sessions.json | 156 --- .../I01_REX/charge-statistics-de.json | 11 - .../I01_REX/charge-statistics-en.json | 11 - .../I01_REX/charging-statistics_1.json | 11 - .../responses/I01_REX/vehicle-charging.json | 427 ------- .../I01_REX/vehicle-fully-charged.json | 423 ------- .../responses/I01_REX/vehicles-de.json | 427 ------- .../resources/responses/I01_REX/vehicles.json | 427 ------- .../responses/I01_REX/vehicles_v2_bmw_0.json | 387 ------- .../responses/ICE/charging_sessions.json | 14 + .../responses/ICE/charging_statistics.json | 8 + .../responses/ICE/vehicles_base.json | 48 + .../responses/ICE/vehicles_state.json | 166 +++ .../responses/ICE2/charging_sessions.json | 14 + .../responses/ICE2/charging_statistics.json | 8 + .../responses/ICE2/vehicles_base.json | 49 + .../responses/ICE2/vehicles_state.json | 155 +++ .../responses/ICE3/charging_sessions.json | 14 + .../responses/ICE3/charging_statistics.json | 8 + .../responses/ICE3/vehicles_base.json | 47 + .../responses/ICE3/vehicles_state.json | 103 ++ .../responses/ICE4/charging_sessions.json | 14 + .../responses/ICE4/charging_statistics.json | 8 + .../responses/ICE4/vehicles_base.json | 48 + .../responses/ICE4/vehicles_state.json | 103 ++ .../responses/MILD_HYBRID/340i_frontView.png | Bin 0 -> 118802 bytes .../responses/MILD_HYBRID/340i_rearView.png | Bin 0 -> 100351 bytes .../MILD_HYBRID/340i_vehicleStatus.jpg | Bin 0 -> 199877 bytes .../MILD_HYBRID/charging_sessions.json | 14 + .../MILD_HYBRID/charging_statistics.json | 8 + .../MILD_HYBRID/remote_service_call.json | 4 + .../MILD_HYBRID/remote_service_error.json | 11 + .../MILD_HYBRID/remote_service_status.json | 3 + .../responses/MILD_HYBRID/vehicles_base.json | 48 + .../responses/MILD_HYBRID/vehicles_state.json | 264 +++++ .../responses/PHEV/vehicles_base.json | 49 + .../responses/PHEV/vehicles_state.json | 223 ++++ .../responses/PHEV2/charging_sessions.json | 50 + .../responses/PHEV2/charging_statistics.json | 11 + .../responses/PHEV2/vehicles_base.json | 49 + .../responses/PHEV2/vehicles_state.json | 256 +++++ .../responses/TwoVehicles/anonymous-raw.json | 386 ------- .../responses/TwoVehicles/f11-raw.json | 283 ----- .../responses/TwoVehicles/two-vehicles.json | 665 ----------- .../chargingprofile/two-weeks-timer.json | 301 ----- .../weekly-planner-t2-active.json | 427 ------- .../remote_services/service-error.json | 2 +- .../test/resources/responses/vehicles.json | 603 ++++++++++ 221 files changed, 12780 insertions(+), 14904 deletions(-) create mode 100644 bundles/org.openhab.binding.mybmw/.gitignore rename bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/{MyBMWConfiguration.java => MyBMWBridgeConfiguration.java} (85%) create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWVehicleConfiguration.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSession.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessions.java rename bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/{ChargeSessionsContainer.java => ChargingSessionsContainer.java} (78%) create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatistics.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsContainer.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/RemoteChargingCommands.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java rename bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/{properties => vehicle}/Address.java (61%) delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CheckControlMessage.java rename bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/{RemoteService.java => ClimateControlState.java} (50%) create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateTimer.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CombustionFuelLevel.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Coordinates.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DepartureTime.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DigitalKey.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DriverPreferences.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ElectricChargingState.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RequiredService.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleAttributes.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBase.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleDoorsState.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleRoofState.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleState.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainer.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireState.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetails.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetailsClassification.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateStatus.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStates.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleWindowsState.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java rename bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/{ => auth}/Token.java (96%) create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/NetworkException.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizer.java rename bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/{ByteResponseCallback.java => enums/ExecutionState.java} (60%) create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java rename bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/{ChargeProfileUtils.java => ChargingProfileUtils.java} (96%) create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileWrapper.java create mode 100644 bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscoveryTest.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java rename bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/{ChargeStatisticWrapper.java => ChargingStatisticsWrapper.java} (67%) delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java rename bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/{handler/ChargeStatisticsTest.java => dto/charge/ChargingStatisticsTest.java} (52%) create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilitiesTest.java create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ConfigurationTest.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ErrorResponseTest.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/SimulationTest.java rename bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/{VehicleTests.java => VehicleHandlerTest.java} (62%) rename bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/{ => auth}/AuthTest.java (89%) create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizerTest.java create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java create mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/530e/vehicles.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/charging_sessions.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/charging_statistics.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV2/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/charging_sessions.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/charging_statistics.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV4/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV4/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV5/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV5/vehicles_state.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/F11/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/F31/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/F44/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/F45/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/F48/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G01/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G05/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G08/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/340i.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-sessions_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-statistics_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/json_export.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-sessions_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-statistics_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-sessions_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-statistics_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/vehicles_v2_bmw_0.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-sessions.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-de.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-en.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charging-statistics_1.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-charging.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-fully-charged.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles-de.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/charging_sessions.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/charging_statistics.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/charging_sessions.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/charging_statistics.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/charging_sessions.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/charging_statistics.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/charging_sessions.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/charging_statistics.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_frontView.png create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_rearView.png create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_vehicleStatus.jpg create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/charging_sessions.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/charging_statistics.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/remote_service_call.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/remote_service_error.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/remote_service_status.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV/vehicles_state.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/charging_sessions.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/charging_statistics.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/vehicles_base.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/vehicles_state.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/anonymous-raw.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/f11-raw.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/two-vehicles.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/two-weeks-timer.json delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json create mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/vehicles.json diff --git a/bundles/org.openhab.binding.mybmw/.gitignore b/bundles/org.openhab.binding.mybmw/.gitignore new file mode 100644 index 0000000000000..b507aafbd8664 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/.gitignore @@ -0,0 +1 @@ +jacoco.exec diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md index b1d608207935f..2e1fe27c79fba 100644 --- a/bundles/org.openhab.binding.mybmw/README.md +++ b/bundles/org.openhab.binding.mybmw/README.md @@ -1,25 +1,25 @@ # MyBMW Binding The binding provides access like [MyBMW App](https://www.bmw.com/en/footer/mybmw-app.html) to openHAB. -All vehicles connected to an account will be detected by the discovery with the correct type: +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 +* 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 +The provided data depends on -1. the [Thing Type](#things) and -1. the [Properties](#properties) mentioned in Services +1. the [Thing Type](#things) and +2. the [Properties](#properties) mentioned in Services Different channel groups are clustering all information. Check for each group if it's supported by your vehicle. -Please note **this isn't a real-time binding**. -If a door is opened the state isn't transmitted and changed immediately. -It's not a flaw in the binding itself because the state in BMW's own MyBMW App is also updated with some delay. +Please note **this isn't a real-time binding**. +If a door is opened the state isn't transmitted and changed immediately. +It's not a flaw in the binding itself because the state in BMW's own MyBMW App is also updated with some delay. ## Supported Things @@ -31,13 +31,14 @@ The bridge establishes the connection between BMW API and openHAB. |----------------------------|----------------|------------------------------------------| | MyBMW Account | `account` | Access to BMW API for a specific user | + ### Things -Four different vehicle types are provided. -They differ in the supported channel groups & channels. -Conventional Fuel Vehicles don't provide e.g. _Charging Profile_, Electric Vehicles don't provide a _Fuel Range_. +Four different vehicle types are provided. +They differ in the supported channel groups & channels. +Conventional Fuel Vehicles don't provide e.g. _Charging Profile_, Electric Vehicles don't provide a _Fuel Range_. For hybrid vehicles in addition to _Fuel and Electric Range_ the _Hybrid Range_ is shown. - + | Name | Thing Type ID | Supported Channel Groups | |-------------------------------------|---------------|---------------------------------------------------------------------| | BMW Electric Vehicle | `bev` | Vehicle with electric drive train | @@ -45,20 +46,21 @@ For hybrid vehicles in addition to _Fuel and Electric Range_ the _Hybrid Range_ | BMW Plug-In-Hybrid Electric Vehicle | `phev` | Vehicle with combustion and electric drive train | | BMW Conventional Vehicle | `conv` | Vehicle with combustion drive train | + #### Properties -For each vehicle properties are available. +For each vehicle properties are available. Basic information is given regarding -- Vehicle properties like model type, drive train and construction year -- Which services are available / not available +* Vehicle properties like model type, drive train and construction year +* Which services are available / not available -In the right picture can see in _remoteServicesEnabled_ e.g. the _Door Lock_ and _Door Unlock_ services are mentioned. +In the right picture can see in *remoteServicesEnabled* e.g. the *Door Lock* and *Door Unlock* services are mentioned. This ensures channel group [Remote Services](#remote-services) is supporting door lock and unlock remote control. -In _Services Supported_ the entry _ChargingHistory_ is mentioned. +In *Services Supported* the entry *ChargingHistory* is mentioned. So it's valid to connect channel group [Charge Sessions](#charge-sessions) in order to display your last charging sessions. | Property Key | Property Value | Supported Channel Groups | @@ -66,11 +68,12 @@ So it's valid to connect channel group [Charge Sessions](#charge-sessions) in or | servicesSupported | ChargingHistory | session | | remoteServicesEnabled | _list of services_ | remote | + ## Discovery -Auto discovery is starting after the bridge is created. +Auto discovery is starting after the bridge is created. A list of your registered vehicles is queried and all found things are added in the inbox. -Unique identifier is the _Vehicle Identification Number_ (VIN). +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. @@ -78,7 +81,7 @@ Properties will be attached to predefined vehicles if the VIN is matching. ### Bridge Configuration -| Parameter | Type | Description | +| Parameter | Type | Description | |-----------------|---------|--------------------------------------------------------------------| | userName | text | MyBMW Username | | password | text | MyBMW Password | @@ -86,49 +89,53 @@ Properties will be attached to predefined vehicles if the VIN is matching. The region Configuration has 3 different options -- _NORTH_AMERICA_ -- _CHINA_ -- _ROW_ (Rest of World) +* _NORTH_AMERICA_ +* _CHINA_ +* _ROW_ (Rest of World) + #### Advanced Configuration -| Parameter | Type | Description | +| Parameter | Type | Description | |-----------------|---------|---------------------------------------------------------| | language | text | Channel data can be returned in the desired language | -Language is predefined as _AUTODETECT_. +Language is predefined as *AUTODETECT*. Some textual descriptions, date and times are delivered based on your local language. You can overwrite this setting with lowercase 2-letter [language code reagrding ISO 639](https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html) -So if want your UI in english language place _en_ as desired language. +So if want your UI in english language place *en* as desired language. ### Thing Configuration Same configuration is needed for all things -| Parameter | Type | Description | +| Parameter | Type | Description | |-----------------|---------|---------------------------------------| | vin | text | Vehicle Identification Number (VIN) | | refreshInterval | integer | Refresh Interval in Minutes | + #### Advanced Configuration -| Parameter | Type | Description | +| Parameter | Type | Description | |-----------------|---------|-----------------------------------| | vehicleBrand | text | Vehicle Brand like BMW or Mini | The _vehicleBrand_ is automatically obtained by the discovery service and shall not be changed. If thing is defined manually via *.things file following brands are supported -- BMW -- MINI +* BMW +* MINI + ## Channels -There are many channels available for each vehicle. +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 + +### Thing Channel Groups | Channel Group ID | Description | conv | phev | bev_rex | bev | |----------------------------------|---------------------------------------------------|------|------|---------|-----| @@ -145,13 +152,14 @@ They differ for each vehicle type, build-in sensors and activated services. | [tires](#tire-pressure) | Current and wanted pressure for all tires | X | X | X | X | | [image](#image) | Provides an image of your vehicle | X | X | X | X | + #### Vehicle Status Reflects overall status of the vehicle. -- Channel Group ID is **status** -- Available for all vehicles -- Read-only values +* Channel Group ID is **status** +* Available for all vehicles +* Read-only values | Channel Label | Channel ID | Type | Description | conv | phev | bev_rex | bev | |---------------------------|---------------------|---------------|------------------------------------------------|------|------|---------|-----| @@ -163,50 +171,50 @@ Reflects overall status of the vehicle. | Check Control | check-control | String | Presence of active warning messages | X | X | X | X | | Plug Connection Status | plug-connection | String | Plug is _Connected_ or _Not connected_ | | X | X | X | | Charging Status | charge | String | Current charging status | | X | X | X | -| Charging Information | charge-info | String | Information regarding current charging session | | X | X | X | -| Motion Status | motion | Switch | Driving state - depends on vehicle hardware | X | X | X | X | -| Last Status Timestamp | last-update | DateTime | Date and time of last status update | X | X | X | X | +| Remaining Charging Time | charge-remaining | Number:Time | Remainining time for current charging session | | X | X | X | +| Last Status Timestamp | last-update | DateTime | Date and time of last status update of the car | X | X | X | X | +| Last Fetched Timestamp | last-fetched | DateTime | Date and time of last time status fetched | X | X | X | X | Overall Door Status values -- _Closed_ - all doors closed -- _Open_ - at least one door is open -- _Undef_ - no door data delivered at all +* _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 +* _Closed_ - all windows closed +* _Open_ - at least one window is completely open +* _Intermediate_ - at least one window is partially open +* _Undef_ - no window data delivered at all Check Control values Localized String of current active warnings. Examples: -- No Issues -- Multiple Issues +* No Issues +* Multiple Issues Charging Status values -- _Not Charging_ -- _Charging_ -- _Plugged In_ -- _Fully Charged_ +* _Not Charging_ +* _Charging_ +* _Plugged In_ +* _Fully Charged_ Charging Information values Localized String of current active charging session Examples -- 100% at ~00:43 -- Starts at ~09:00 +* 100% at ~00:43 +* Starts at ~09:00 ##### Vehicle Status Raw Data The _raw data channel_ is marked as _advanced_ and isn't shown by default. Target are advanced users to derive even more data out of BMW API replies. -As the replies are formatted as JSON use the [JsonPath Transformation Service](https://www.openhab.org/addons/transformations/jsonpath/) to extract data for an item, +As the replies are formatted as JSON use the [JsonPath Transformation Service](https://www.openhab.org/addons/transformations/jsonpath/) to extract data for an item, | Channel Label | Channel ID | Type | Description | |---------------------------|---------------------|---------------|------------------------------------------------| @@ -216,50 +224,53 @@ As the replies are formatted as JSON use the [JsonPath Transformation Service](h Examples: -###### Country ISO Code +_Country ISO Code_ -```json +``` $.properties.originCountryISO ``` -###### Drivers Guide URL +_Drivers Guide URL_ -```json +``` $.driverGuideInfo.androidStoreUrl ``` #### Range Data -Based on vehicle type some channels are present or not. -Conventional fuel vehicles don't provide _Electric Range_ and battery electric vehicles don't show _Fuel Range_. -Hybrid vehicles have both and in addition _Hybrid Range_. +Based on vehicle type some channels are present or not. +Conventional fuel vehicles don't provide *Electric Range* and battery electric vehicles don't show *Fuel Range*. +Hybrid vehicles have both and in addition *Hybrid Range*. See description [Range vs Range Radius](#range-vs-range-radius) to get more information. -- Channel Group ID is **range** -- Availability according to table -- Read-only values - -| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev | -|---------------------------|-------------------------|----------------------|------|------|---------|-----| -| Mileage | mileage | Number:Length | X | X | X | X | -| Fuel Range | range-fuel | Number:Length | X | X | X | | -| Electric Range | range-electric | Number:Length | | X | X | X | -| Hybrid Range | range-hybrid | Number:Length | | X | X | | -| Battery Charge Level | soc | Number:Dimensionless | | X | X | X | -| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | | -| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | | -| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X | -| Hybrid Range Radius | range-radius-hybrid | Number:Length | | X | X | | +* Channel Group ID is **range** +* Availability according to table +* Read-only values + +| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev | +|------------------------------------|----------------------------|----------------------|------|------|---------|-----| +| Mileage | mileage | Number:Length | X | X | X | X | +| Fuel Range | range-fuel | Number:Length | X | X | X | | +| Electric Range | range-electric | Number:Length | | X | X | X | +| Hybrid Range | range-hybrid | Number:Length | | X | X | | +| Battery Charge Level | soc | Number:Dimensionless | | X | X | X | +| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | | +| Estimated Fuel Consumption l/100km | estimated-fuel-l-100km | Number:Dimensionless | X | X | X | | +| Estimated Fuel Consumption mpg | estimated-fuel-mpg | Number:Dimensionless | X | X | X | | +| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | | +| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X | +| Hybrid Range Radius | range-radius-hybrid | Number:Length | | X | X | | + #### Doors Details Detailed status of all doors and windows. -- Channel Group ID is **doors** -- Available for all vehicles if corresponding sensors are built-in -- Read-only values - -| Channel Label | Channel ID | Type | +* 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 | @@ -276,20 +287,21 @@ Detailed status of all doors and windows. 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 +* _Undef_ - no status data available +* _Invalid_ - this door / window isn't applicable for this vehicle +* _Closed_ - the door / window is closed +* _Open_ - the door / window is open +* _Intermediate_ - window in intermediate position, not applicable for doors + #### Check Control Group for all current active Check Control messages. If more than one message is active the channel _name_ contains all active messages as options. -- Channel Group ID is **check** -- Available for all vehicles -- Read/Write access +* Channel Group ID is **check** +* Available for all vehicles +* Read/Write access | Channel Label | Channel ID | Type | Access | |---------------------------------|---------------------|----------------|------------| @@ -299,18 +311,19 @@ If more than one message is active the channel _name_ contains all active messag Severity Levels -- Ok -- Low -- Medium +* Ok +* Low +* Medium + #### Services Group for all upcoming services with description, service date and/or service mileage. If more than one service is scheduled in the future the channel _name_ contains all future services as options. -- Channel Group ID is **service** -- Available for all vehicles -- Read/Write access +* Channel Group ID is **service** +* Available for all vehicles +* Read/Write access | Channel Label | Channel ID | Type | Access | |--------------------------------|---------------------|----------------|------------| @@ -319,31 +332,33 @@ If more than one service is scheduled in the future the channel _name_ contains | Service Date | date | DateTime | Read | | Mileage till Service | mileage | Number:Length | Read | + #### Location GPS location and heading of the vehicle. -- Channel Group ID is **location** -- Available for all vehicles with built-in GPS sensor. Function can be enabled/disabled in the head unit -- Read-only values +* Channel 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 | +| Channel Label | Channel ID | Type | |---------------------|---------------------|---------------| -| GPS Coordinates | gps | Location | -| Heading | heading | Number:Angle | -| Address | address | String | -| Distance from Home | home-distance | Number:Length | +| GPS Coordinates | gps | Location | +| Heading | heading | Number:Angle | +| Address | address | String | +| Distance from Home | home-distance | Number:Length | #### Remote Services -Remote control of the vehicle. -Send a _command_ to the vehicle and the _state_ is reporting the execution progress. +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 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 | |-------------------------|---------------------|---------|--------| @@ -352,82 +367,86 @@ Parallel execution isn't supported. The channel _command_ provides options -- _light-flash_ -- _vehicle-finder_ -- _door-lock_ -- _door-unlock_ -- _horn-blow_ -- _climate-now-start_ -- _climate-now-stop_ +* _light-flash_ +* _vehicle-finder_ +* _door-lock_ +* _door-unlock_ +* _horn-blow_ +* _climate-now-start_ +* _climate-now-stop_ +* _charge-now_ The channel _state_ shows the progress of the command execution in the following order -1. _initiated_ -1. _pending_ -1. _delivered_ -1. _executed_ +1) _initiated_ +2) _pending_ +3) _delivered_ +4) _executed_ + #### Charge Profile Charging options with date and time for preferred time windows and charging modes. -- Channel Group ID is **profile** -- Available for electric and hybrid vehicles -- Read access for UI. -- There are 4 timers _T1, T2, T3 and T4_ available. Replace _X_ with number 1,2 or 3 to target the correct timer - -| Channel Label | Channel ID | Type | -|----------------------------|---------------------------|----------| -| Charge Mode | mode | String | -| Charge Preferences | prefs | String | -| Charging Plan | control | String | -| SoC Target | target | String | -| Charging Energy Limited | limit | Switch | -| Window Start Time | window-start | DateTime | -| Window End Time | window-end | DateTime | -| A/C at Departure | climate | Switch | -| T_X_ Enabled | timer_X_-enabled | Switch | -| T_X_ Departure Time | timer_X_-departure | DateTime | -| T_X_ Monday | timer_X_-day-mon | Switch | -| T_X_ Tuesday | timer_X_-day-tue | Switch | -| T_X_ Wednesday | timer_X_-day-wed | Switch | -| T_X_ Thursday | timer_X_-day-thu | Switch | -| T_X_ Friday | timer_X_-day-fri | Switch | -| T_X_ Saturday | timer_X_-day-sat | Switch | -| T_X_ Sunday | timer_X_-day-sun | Switch | +* Channel Group ID is **profile** +* Available for electric and hybrid vehicles +* Read access for UI. +* There are 4 timers *T1, T2, T3 and T4* available. Replace *X* with number 1,2 or 3 to target the correct timer + +| Channel Label | Channel ID | Type | +|----------------------------|---------------------------|----------| +| Charge Mode | mode | String | +| Charge Preferences | prefs | String | +| Charging Plan | control | String | +| SoC Target | target | String | +| Charging Energy Limited | limit | Switch | +| Window Start Time | window-start | DateTime | +| Window End Time | window-end | DateTime | +| A/C at Departure | climate | Switch | +| T*X* Enabled | timer*X*-enabled | Switch | +| T*X* Departure Time | timer*X*-departure | DateTime | +| T*X* Monday | timer*X*-day-mon | Switch | +| T*X* Tuesday | timer*X*-day-tue | Switch | +| T*X* Wednesday | timer*X*-day-wed | Switch | +| T*X* Thursday | timer*X*-day-thu | Switch | +| T*X* Friday | timer*X*-day-fri | Switch | +| T*X* Saturday | timer*X*-day-sat | Switch | +| T*X* Sunday | timer*X*-day-sun | Switch | The channel _profile-mode_ supports -- _immediateCharging_ -- _delayedCharging_ +* *immediateCharging* +* *delayedCharging* The channel _profile-prefs_ supports -- _noPreSelection_ -- _chargingWindow_ +* *noPreSelection* +* *chargingWindow* + #### Charge Statistics Shows charge statistics of the current month -- Channel Group ID is **statistic** -- Available for electric and hybrid vehicles -- Read-only values - -| Channel Label | Channel ID | Type | +* Channel Group ID is **statistic** +* Available for electric and hybrid vehicles +* Read-only values + +| Channel Label | Channel ID | Type | |----------------------------|-------------------------|----------------| | Charge Statistic Month | title | String | | Energy Charged | energy | Number:Energy | | Charge Sessions | sessions | Number | + #### Charge Sessions Group for past charging sessions. If more than one message is active the channel _name_ contains all active messages as options. -- Channel Group ID is **session** -- Available for electric and hybrid vehicles -- Read-only values +* Channel Group ID is **session** +* Available for electric and hybrid vehicles +* Read-only values | Channel Label | Channel ID | Type | |---------------------------------|--------------|----------| @@ -437,15 +456,16 @@ If more than one message is active the channel _name_ contains all active messag | Issues during Session | issue | String | | Session Status | status | String | + #### Tire Pressure Current and target tire pressure values -- Channel Group ID is **tires** -- Available for all vehicles if corresponding sensors are built-in -- Read-only values - -| Channel Label | Channel ID | Type | +* Channel Group ID is **tires** +* Available for all vehicles if corresponding sensors are built-in +* Read-only values + +| Channel Label | Channel ID | Type | |----------------------------|-------------------------|------------------| | Front Left | fl-current | Number:Pressure | | Front Left Target | fl-target | Number:Pressure | @@ -456,13 +476,14 @@ Current and target tire pressure values | Rear Right | rr-current | Number:Pressure | | Rear Right Target | rr-target | Number:Pressure | + #### Image -Image representation of the vehicle. +Image representation of the vehicle. -- Channel Group ID is **image** -- Available for all vehicles -- Read/Write access +* Channel Group ID is **image** +* Available for all vehicles +* Read/Write access | Channel Label | Channel ID | Type | Access | |----------------------------|---------------------|--------|----------| @@ -471,10 +492,12 @@ Image representation of the vehicle. Possible view ports: -- _VehicleStatus_ Front Side View -- _VehicleInfo_ Front View -- _ChargingHistory_ Side View -- _Default_ Front Side View +* _VehicleStatus_ Front Left Side View +* _FrontView_ Front View +* _FrontLeft_ Front Left Side View +* _FrontRight_ Front Right Side View +* _RearView_ Rear View + ## Further Descriptions @@ -484,83 +507,88 @@ Possible view ports: There are 3 occurrences of dynamic data delivered -- Upcoming Services delivered in group [Services](#services) -- Check Control Messages delivered in group [Check Control](#check-control) -- Charging Session data delivered in group [Charge Sessions](#charge-sessions) +* Upcoming Services delivered in group [Services](#services) +* Check Control Messages delivered in group [Check Control](#check-control) +* Charging Session data delivered in group [Charge Sessions](#charge-sessions) -The channel id _name_ shows the first element as default. -All other possibilities are attached as options. -The picture on the right shows the _Session Title_ item and 3 possible options. +The channel id _name_ shows the first element as default. +All other possibilities are attached as options. +The picture on the right shows the _Session Title_ item and 3 possible options. Select the desired service and the corresponding Charge Session with _Energy Charged_, _Session Status_ and _Session Issues_ will be shown. ### TroubleShooting BMW has a high range of vehicles supported by their API. -In case of any issues with this binding help to resolve it! +In case of any issues with this binding help to resolve it! Please perform the following steps: -- Can you log into MyBMW App with your credentials? -- Is the vehicle listed in your account? -- Is the [MyBMW Brige](#bridge) status _Online_? +* Can you log into MyBMW App with your credentials? +* Is the vehicle listed in your account? +* Is the [MyBMW Brige](#bridge) status _Online_? -If these preconditions are fulfilled proceed with the fingerprint generation. +If these preconditions are fulfilled proceed with the fingerprint generation. #### Generate Debug Fingerprint - +Login to the openHAB console and use the `mybmw fingerprint` command. -First [enable debug logging](https://www.openhab.org/docs/administration/logging.html#defining-what-to-log) for the binding. +Fingerprint information on your account and vehicle(s) will show in the console and can be copied from there. +A zip file with fingerprint information for your vehicle(s) will also be generated and put in the `mybmw` folder in your home directory. +This fingerprint information is valuable for the developers to better support your vehicle in the software. -```shell -log:set DEBUG org.openhab.binding.mybmw -``` +You can restrict the accounts and vehicles for the fingerprint generation. +Full syntax is available through the `mybmw help` console command. -The debug fingerprint is generated every time the discovery is executed. -To force a new fingerprint perform a _Scan_ for MyBMW things. -Personal data is eliminated from the log entries so it should be possible to share them in public. +Personal data is eliminated from fingerprints so it should be possible to share them in public. Data like -- Vehicle Identification Number (VIN) -- Location data +* Vehicle Identification Number (VIN) +* Location data -are anonymized. -You'll find the fingerprint in the logs with the command +are anonymized in the JSON response and URL's. -```shell -grep "Discovery Fingerprint Data" openhab.log -``` +After the corresponding fingerprint is generated please [follow the instructions to raise an issue](https://community.openhab.org/t/how-to-file-an-issue/68464) and attach the fingerprint! -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! +#### Debug Logging + +You can [enable debug logging](https://www.openhab.org/docs/administration/logging.html#defining-what-to-log) to get more information on the behaviour of the binding. + +``` +log:set DEBUG org.openhab.binding.mybmw +``` + +As with fingerprint data, personal data is eliminated from logs. + ### Range vs Range Radius -You will observe differences in the vehicle range and range radius values. +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. +The right picture shows the distance between Kassel and Frankfurt in Germany. While the air-line distance is 145 kilometers the route distance is 192 kilometers. So range value is the normal remaining range while the range radius values can be used e.g. on [Mapview](https://www.openhab.org/docs/ui/sitemaps.html#element-type-mapview) to indicate the reachable range on map. Please note this is just an indicator of the effective range. -Especially for electric vehicles it depends on many factors like driving style and usage of electric consumers. +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). +The example is based on a BMW i3 with range extender (REX). Exchange configuration parameters in the Things section -- 4711 - any id you want -- YOUR_USERNAME - with your MyBMW login username -- YOUR_PASSWORD - with your MyBMW password credentials -- VEHICLE_VIN - the vehicle identification number +* 4711 - any id you want +* YOUR_USERNAME - with your MyBMW login username +* YOUR_PASSWORD - with your MyBMW password credentials +* VEHICLE_VIN - the vehicle identification number -In addition search for all occurrences of _i3_ and replace it with your Vehicle Identification like _x3_ or _535d_ and you're ready to go! +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 -```java +``` Bridge mybmw:account:4711 "MyBMW Account" [userName="YOUR_USERNAME",password="YOUR_PASSWORD",region="ROW"] { Thing bev_rex i3 "BMW i3 94h REX" [ vin="VEHICLE_VIN",refreshInterval=5,vehicleBrand="BMW"] } @@ -568,7 +596,7 @@ Bridge mybmw:account:4711 "MyBMW Account" [userName="YOUR_USERNAME",password=" ### Items File -```java +``` Number:Length i3Mileage "Odometer [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#mileage" } Number:Length i3Range "Range [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#hybrid"} Number:Length i3RangeElectric "Electric Range [%d %unit%]" (i3,long) {channel="mybmw:bev_rex:4711:i3:range#electric"} @@ -689,7 +717,7 @@ String i3ImageViewport "Image Viewport [%s]" ### Sitemap File -```perl +``` sitemap BMW label="BMW" { Frame label="BMW i3" { Image item=i3Image @@ -821,4 +849,5 @@ sitemap BMW label="BMW" { ## Credits -This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected). +This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected). + diff --git a/bundles/org.openhab.binding.mybmw/pom.xml b/bundles/org.openhab.binding.mybmw/pom.xml index d6471a8159bb6..b180259cb1aaa 100644 --- a/bundles/org.openhab.binding.mybmw/pom.xml +++ b/bundles/org.openhab.binding.mybmw/pom.xml @@ -14,4 +14,172 @@ openHAB Add-ons :: Bundles :: MyBMW Binding + + + + org.jacoco + org.jacoco.agent + runtime + 0.8.8 + test + + + + + + + test-coverage + + + + org.apache.maven.plugins + maven-surefire-plugin + + + target/jacoco.exec + + + + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + default-instrument + + instrument + + + + default-restore-instrumented-classes + test + + restore-instrumented-classes + + + + default-report + test + + report + + + + default-check + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.20 + + + BRANCH + COVEREDRATIO + 0.20 + + + + + + + + + + + + + + integration-tests + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M7 + + + + integration-test + verify + + + + + + + + + + test-jar + + + + maven-resources-plugin + 3.3.0 + + + copy-resources + + package + + copy-resources + + + ${basedir}/target/classes + + + src/test/resources + true + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + package + + jar + + + testenv + + + + + + + + diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConfiguration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWBridgeConfiguration.java similarity index 85% rename from bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConfiguration.java rename to bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWBridgeConfiguration.java index 6a548795936d8..2d5f16272665d 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConfiguration.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWBridgeConfiguration.java @@ -16,12 +16,13 @@ import org.openhab.binding.mybmw.internal.utils.Constants; /** - * The {@link MyBMWConfiguration} class contains fields mapping thing configuration parameters. + * The {@link MyBMWBridgeConfiguration} class contains fields mapping thing configuration parameters. * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - renamed */ @NonNullByDefault -public class MyBMWConfiguration { +public class MyBMWBridgeConfiguration { /** * Depending on the location the correct server needs to be called diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java index 3f1e741a4a575..b930e4b6f1991 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java @@ -23,20 +23,21 @@ * * @author Bernd Weymann - Initial contribution * @author Norbert Truchsess - edit & send of charge profile + * @author Martin Grassl - updated enum values */ @NonNullByDefault -public class MyBMWConstants { +public interface MyBMWConstants { - private static final String BINDING_ID = "mybmw"; + static final String BINDING_ID = "mybmw"; - public static final String VIN = "vin"; + static final String VIN = "vin"; - public static final int DEFAULT_IMAGE_SIZE_PX = 1024; - public static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 5; + static final int DEFAULT_IMAGE_SIZE_PX = 1024; + static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 5; // See constants from bimmer-connected // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/vehicle.py - public enum VehicleType { + enum VehicleType { CONVENTIONAL("conv"), PLUGIN_HYBRID("phev"), MILD_HYBRID("hybrid"), @@ -56,150 +57,150 @@ public String toString() { } } - public enum ChargingMode { - immediateCharging, - delayedCharging + enum ChargingMode { + IMMEDIATE_CHARGING, + DELAYED_CHARGING } - public enum ChargingPreference { - noPreSelection, - chargingWindow + enum ChargingPreference { + NO_PRESELECTION, + CHARGING_WINDOW } - public static final Set FUEL_VEHICLES = Set.of(VehicleType.CONVENTIONAL.toString(), + static final Set FUEL_VEHICLES = Set.of(VehicleType.CONVENTIONAL.toString(), VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString()); - public static final Set ELECTRIC_VEHICLES = Set.of(VehicleType.ELECTRIC.toString(), + static final Set ELECTRIC_VEHICLES = Set.of(VehicleType.ELECTRIC.toString(), VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString()); // List of all Thing Type UIDs - public static final ThingTypeUID THING_TYPE_CONNECTED_DRIVE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); - public static final ThingTypeUID THING_TYPE_CONV = new ThingTypeUID(BINDING_ID, - VehicleType.CONVENTIONAL.toString()); - public static final ThingTypeUID THING_TYPE_PHEV = new ThingTypeUID(BINDING_ID, - VehicleType.PLUGIN_HYBRID.toString()); - public static final ThingTypeUID THING_TYPE_BEV_REX = new ThingTypeUID(BINDING_ID, - VehicleType.ELECTRIC_REX.toString()); - public static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC.toString()); - public static final Set SUPPORTED_THING_SET = Set.of(THING_TYPE_CONNECTED_DRIVE_ACCOUNT, - THING_TYPE_CONV, THING_TYPE_PHEV, THING_TYPE_BEV_REX, THING_TYPE_BEV); + static final ThingTypeUID THING_TYPE_CONNECTED_DRIVE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); + static final ThingTypeUID THING_TYPE_CONV = new ThingTypeUID(BINDING_ID, VehicleType.CONVENTIONAL.toString()); + static final ThingTypeUID THING_TYPE_PHEV = new ThingTypeUID(BINDING_ID, VehicleType.PLUGIN_HYBRID.toString()); + static final ThingTypeUID THING_TYPE_BEV_REX = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC_REX.toString()); + static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC.toString()); + static final Set SUPPORTED_THING_SET = Set.of(THING_TYPE_CONNECTED_DRIVE_ACCOUNT, THING_TYPE_CONV, + THING_TYPE_PHEV, THING_TYPE_BEV_REX, THING_TYPE_BEV); // Thing Group definitions - public static final String CHANNEL_GROUP_STATUS = "status"; - public static final String CHANNEL_GROUP_SERVICE = "service"; - public static final String CHANNEL_GROUP_CHECK_CONTROL = "check"; - public static final String CHANNEL_GROUP_DOORS = "doors"; - public static final String CHANNEL_GROUP_RANGE = "range"; - public static final String CHANNEL_GROUP_LOCATION = "location"; - public static final String CHANNEL_GROUP_REMOTE = "remote"; - public static final String CHANNEL_GROUP_CHARGE_PROFILE = "profile"; - public static final String CHANNEL_GROUP_CHARGE_STATISTICS = "statistic"; - public static final String CHANNEL_GROUP_CHARGE_SESSION = "session"; - public static final String CHANNEL_GROUP_TIRES = "tires"; - public static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image"; + static final String CHANNEL_GROUP_STATUS = "status"; + static final String CHANNEL_GROUP_SERVICE = "service"; + static final String CHANNEL_GROUP_CHECK_CONTROL = "check"; + static final String CHANNEL_GROUP_DOORS = "doors"; + static final String CHANNEL_GROUP_RANGE = "range"; + static final String CHANNEL_GROUP_LOCATION = "location"; + static final String CHANNEL_GROUP_REMOTE = "remote"; + static final String CHANNEL_GROUP_CHARGE_PROFILE = "profile"; + static final String CHANNEL_GROUP_CHARGE_STATISTICS = "statistic"; + static final String CHANNEL_GROUP_CHARGE_SESSION = "session"; + static final String CHANNEL_GROUP_TIRES = "tires"; + static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image"; // Charge Statistics & Sessions - public static final String SESSIONS = "sessions"; - public static final String ENERGY = "energy"; - public static final String TITLE = "title"; - public static final String SUBTITLE = "subtitle"; - public static final String ISSUE = "issue"; - public static final String STATUS = "status"; + static final String SESSIONS = "sessions"; + static final String ENERGY = "energy"; + static final String TITLE = "title"; + static final String SUBTITLE = "subtitle"; + static final String ISSUE = "issue"; + static final String STATUS = "status"; // Generic Constants for several groups - public static final String NAME = "name"; - public static final String DETAILS = "details"; - public static final String SEVERITY = "severity"; - public static final String DATE = "date"; - public static final String MILEAGE = "mileage"; - public static final String GPS = "gps"; - public static final String HEADING = "heading"; - public static final String ADDRESS = "address"; - public static final String HOME_DISTANCE = "home-distance"; + static final String NAME = "name"; + static final String DETAILS = "details"; + static final String SEVERITY = "severity"; + static final String DATE = "date"; + static final String MILEAGE = "mileage"; + static final String GPS = "gps"; + static final String HEADING = "heading"; + static final String ADDRESS = "address"; + static final String HOME_DISTANCE = "home-distance"; // Status - public static final String DOORS = "doors"; - public static final String WINDOWS = "windows"; - public static final String LOCK = "lock"; - public static final String SERVICE_DATE = "service-date"; - public static final String SERVICE_MILEAGE = "service-mileage"; - public static final String CHECK_CONTROL = "check-control"; - public static final String PLUG_CONNECTION = "plug-connection"; - public static final String CHARGE_STATUS = "charge"; - public static final String CHARGE_INFO = "charge-info"; - public static final String MOTION = "motion"; - public static final String LAST_UPDATE = "last-update"; - public static final String RAW = "raw"; + static final String DOORS = "doors"; + static final String WINDOWS = "windows"; + static final String LOCK = "lock"; + static final String SERVICE_DATE = "service-date"; + static final String SERVICE_MILEAGE = "service-mileage"; + static final String CHECK_CONTROL = "check-control"; + static final String PLUG_CONNECTION = "plug-connection"; + static final String CHARGE_STATUS = "charge"; + static final String CHARGE_REMAINING = "charge-remaining"; + static final String LAST_UPDATE = "last-update"; + static final String LAST_FETCHED = "last-fetched"; + static final String RAW = "raw"; // Door Details - public static final String DOOR_DRIVER_FRONT = "driver-front"; - public static final String DOOR_DRIVER_REAR = "driver-rear"; - public static final String DOOR_PASSENGER_FRONT = "passenger-front"; - public static final String DOOR_PASSENGER_REAR = "passenger-rear"; - public static final String HOOD = "hood"; - public static final String TRUNK = "trunk"; - public static final String WINDOW_DOOR_DRIVER_FRONT = "win-driver-front"; - public static final String WINDOW_DOOR_DRIVER_REAR = "win-driver-rear"; - public static final String WINDOW_DOOR_PASSENGER_FRONT = "win-passenger-front"; - public static final String WINDOW_DOOR_PASSENGER_REAR = "win-passenger-rear"; - public static final String WINDOW_REAR = "win-rear"; - public static final String SUNROOF = "sunroof"; + static final String DOOR_DRIVER_FRONT = "driver-front"; + static final String DOOR_DRIVER_REAR = "driver-rear"; + static final String DOOR_PASSENGER_FRONT = "passenger-front"; + static final String DOOR_PASSENGER_REAR = "passenger-rear"; + static final String HOOD = "hood"; + static final String TRUNK = "trunk"; + static final String WINDOW_DOOR_DRIVER_FRONT = "win-driver-front"; + static final String WINDOW_DOOR_DRIVER_REAR = "win-driver-rear"; + static final String WINDOW_DOOR_PASSENGER_FRONT = "win-passenger-front"; + static final String WINDOW_DOOR_PASSENGER_REAR = "win-passenger-rear"; + static final String WINDOW_REAR = "win-rear"; + static final String SUNROOF = "sunroof"; // Charge Profile - public static final String CHARGE_PROFILE_CLIMATE = "climate"; - public static final String CHARGE_PROFILE_MODE = "mode"; - public static final String CHARGE_PROFILE_PREFERENCE = "prefs"; - public static final String CHARGE_PROFILE_CONTROL = "control"; - public static final String CHARGE_PROFILE_TARGET = "target"; - public static final String CHARGE_PROFILE_LIMIT = "limit"; - public static final String CHARGE_WINDOW_START = "window-start"; - public static final String CHARGE_WINDOW_END = "window-end"; - public static final String CHARGE_TIMER1 = "timer1"; - public static final String CHARGE_TIMER2 = "timer2"; - public static final String CHARGE_TIMER3 = "timer3"; - public static final String CHARGE_TIMER4 = "timer4"; - public static final String CHARGE_DEPARTURE = "-departure"; - public static final String CHARGE_ENABLED = "-enabled"; - public static final String CHARGE_DAY_MON = "-day-mon"; - public static final String CHARGE_DAY_TUE = "-day-tue"; - public static final String CHARGE_DAY_WED = "-day-wed"; - public static final String CHARGE_DAY_THU = "-day-thu"; - public static final String CHARGE_DAY_FRI = "-day-fri"; - public static final String CHARGE_DAY_SAT = "-day-sat"; - public static final String CHARGE_DAY_SUN = "-day-sun"; + static final String CHARGE_PROFILE_CLIMATE = "climate"; + static final String CHARGE_PROFILE_MODE = "mode"; + static final String CHARGE_PROFILE_PREFERENCE = "prefs"; + static final String CHARGE_PROFILE_CONTROL = "control"; + static final String CHARGE_PROFILE_TARGET = "target"; + static final String CHARGE_PROFILE_LIMIT = "limit"; + static final String CHARGE_WINDOW_START = "window-start"; + static final String CHARGE_WINDOW_END = "window-end"; + static final String CHARGE_TIMER1 = "timer1"; + static final String CHARGE_TIMER2 = "timer2"; + static final String CHARGE_TIMER3 = "timer3"; + static final String CHARGE_TIMER4 = "timer4"; + static final String CHARGE_DEPARTURE = "-departure"; + static final String CHARGE_ENABLED = "-enabled"; + static final String CHARGE_DAY_MON = "-day-mon"; + static final String CHARGE_DAY_TUE = "-day-tue"; + static final String CHARGE_DAY_WED = "-day-wed"; + static final String CHARGE_DAY_THU = "-day-thu"; + static final String CHARGE_DAY_FRI = "-day-fri"; + static final String CHARGE_DAY_SAT = "-day-sat"; + static final String CHARGE_DAY_SUN = "-day-sun"; // Range - public static final String RANGE_ELECTRIC = "electric"; - public static final String RANGE_RADIUS_ELECTRIC = "radius-electric"; - public static final String RANGE_FUEL = "fuel"; - public static final String RANGE_RADIUS_FUEL = "radius-fuel"; - public static final String RANGE_HYBRID = "hybrid"; - public static final String RANGE_RADIUS_HYBRID = "radius-hybrid"; - public static final String REMAINING_FUEL = "remaining-fuel"; - public static final String SOC = "soc"; + static final String RANGE_ELECTRIC = "electric"; + static final String RANGE_RADIUS_ELECTRIC = "radius-electric"; + static final String RANGE_FUEL = "fuel"; + static final String RANGE_RADIUS_FUEL = "radius-fuel"; + static final String RANGE_HYBRID = "hybrid"; + static final String RANGE_RADIUS_HYBRID = "radius-hybrid"; + static final String REMAINING_FUEL = "remaining-fuel"; + static final String ESTIMATED_FUEL_L_100KM = "estimated-fuel-l-100km"; + static final String ESTIMATED_FUEL_MPG = "estimated-fuel-mpg"; + static final String SOC = "soc"; // Image - public static final String IMAGE_FORMAT = "png"; - public static final String IMAGE_VIEWPORT = "view"; + static final String IMAGE_FORMAT = "png"; + static final String IMAGE_VIEWPORT = "view"; // Remote Services - public static final String REMOTE_SERVICE_LIGHT_FLASH = "light-flash"; - public static final String REMOTE_SERVICE_VEHICLE_FINDER = "vehicle-finder"; - public static final String REMOTE_SERVICE_DOOR_LOCK = "door-lock"; - public static final String REMOTE_SERVICE_DOOR_UNLOCK = "door-unlock"; - public static final String REMOTE_SERVICE_HORN = "horn-blow"; - public static final String REMOTE_SERVICE_AIR_CONDITIONING_START = "climate-now-start"; - public static final String REMOTE_SERVICE_AIR_CONDITIONING_STOP = "climate-now-stop"; - - public static final String REMOTE_SERVICE_COMMAND = "command"; - public static final String REMOTE_STATE = "state"; + static final String REMOTE_SERVICE_LIGHT_FLASH = "light-flash"; + static final String REMOTE_SERVICE_VEHICLE_FINDER = "vehicle-finder"; + static final String REMOTE_SERVICE_DOOR_LOCK = "door-lock"; + static final String REMOTE_SERVICE_DOOR_UNLOCK = "door-unlock"; + static final String REMOTE_SERVICE_HORN = "horn-blow"; + static final String REMOTE_SERVICE_AIR_CONDITIONING_START = "climate-now-start"; + static final String REMOTE_SERVICE_AIR_CONDITIONING_STOP = "climate-now-stop"; + static final String REMOTE_SERVICE_CHARGE = "charge-now"; + + static final String REMOTE_SERVICE_COMMAND = "command"; + static final String REMOTE_STATE = "state"; // TIRES - public static final String FRONT_LEFT_CURRENT = "fl-current"; - public static final String FRONT_LEFT_TARGET = "fl-target"; - public static final String FRONT_RIGHT_CURRENT = "fr-current"; - public static final String FRONT_RIGHT_TARGET = "fr-target"; - public static final String REAR_LEFT_CURRENT = "rl-current"; - public static final String REAR_LEFT_TARGET = "rl-target"; - public static final String REAR_RIGHT_CURRENT = "rr-current"; - public static final String REAR_RIGHT_TARGET = "rr-target"; + static final String FRONT_LEFT_CURRENT = "fl-current"; + static final String FRONT_LEFT_TARGET = "fl-target"; + static final String FRONT_RIGHT_CURRENT = "fr-current"; + static final String FRONT_RIGHT_TARGET = "fr-target"; + static final String REAR_LEFT_CURRENT = "rl-current"; + static final String REAR_LEFT_TARGET = "rl-target"; + static final String REAR_RIGHT_CURRENT = "rr-current"; + static final String REAR_RIGHT_TARGET = "rr-target"; } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWVehicleConfiguration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWVehicleConfiguration.java new file mode 100644 index 0000000000000..1d29d45ec338b --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWVehicleConfiguration.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mybmw.internal.utils.Constants; + +/** + * The {@link MyBMWVehicleConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - renaming and refactoring to Java Beans + */ +@NonNullByDefault +public class MyBMWVehicleConfiguration { + /** + * Vehicle Identification Number (VIN) + */ + private String vin = Constants.EMPTY; + + /** + * Vehicle brand + * - bmw + * - bmw_i + * - mini + */ + private String vehicleBrand = Constants.EMPTY; + + /** + * Data refresh rate in minutes + */ + private int refreshInterval = MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES; + + /** + * @return the vin + */ + public String getVin() { + return vin; + } + + /** + * @param vin the vin to set + */ + public void setVin(String vin) { + this.vin = vin; + } + + /** + * @return the vehicleBrand + */ + public String getVehicleBrand() { + return vehicleBrand; + } + + /** + * @param vehicleBrand the vehicleBrand to set + */ + public void setVehicleBrand(String vehicleBrand) { + this.vehicleBrand = vehicleBrand; + } + + /** + * @return the refreshInterval + */ + public int getRefreshInterval() { + return refreshInterval; + } + + /** + * @param refreshInterval the refreshInterval to set + */ + public void setRefreshInterval(int refreshInterval) { + this.refreshInterval = refreshInterval; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return "MyBMWVehicleConfiguration [vin=" + vin + ", vehicleBrand=" + vehicleBrand + ", refreshInterval=" + + refreshInterval + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java deleted file mode 100644 index 5c7043ebf2f18..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/VehicleConfiguration.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.mybmw.internal.utils.Constants; - -/** - * The {@link VehicleConfiguration} class contains fields mapping thing configuration parameters. - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class VehicleConfiguration { - /** - * Vehicle Identification Number (VIN) - */ - public String vin = Constants.EMPTY; - - /** - * Vehicle brand - * - bmw - * - mini - */ - public String vehicleBrand = Constants.EMPTY; - - /** - * Data refresh rate in minutes - */ - public int refreshInterval = MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java new file mode 100644 index 0000000000000..3a1b5f70e9954 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java @@ -0,0 +1,325 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.console; + +import static org.openhab.binding.mybmw.internal.MyBMWConstants.*; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; +import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler; +import org.openhab.binding.mybmw.internal.handler.backend.NetworkException; +import org.openhab.binding.mybmw.internal.handler.backend.ResponseContentAnonymizer; +import org.openhab.binding.mybmw.internal.utils.BimmerConstants; +import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.ConsoleCommandCompleter; +import org.openhab.core.io.console.StringsCompleter; +import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link MyBMWCommandExtension} is responsible for handling console commands + * + * @author Mark Herwege - Initial contribution + */ + +@NonNullByDefault +@Component(service = ConsoleCommandExtension.class) +public class MyBMWCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private static final String FINGERPRINT_ROOT_PATH = System.getProperty("user.home") + File.separator + BINDING_ID; + + private static final String FINGERPRINT = "fingerprint"; + private static final StringsCompleter CMD_COMPLETER = new StringsCompleter(List.of(FINGERPRINT), false); + + private final ThingRegistry thingRegistry; + + @Activate + public MyBMWCommandExtension(final @Reference ThingRegistry thingRegistry) { + super("mybmw", "Interact with the MyBMW binding"); + this.thingRegistry = thingRegistry; + } + + @Override + public void execute(String[] args, Console console) { + if ((args.length < 1) || (args.length > 3)) { + console.println("Invalid number of arguments"); + printUsage(console); + return; + } + + List bridgeHandlers = thingRegistry.stream() + .filter(t -> THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(t.getThingTypeUID())) + .map(b -> ((MyBMWBridgeHandler) b.getHandler())).filter(Objects::nonNull).collect(Collectors.toList()); + if (bridgeHandlers.isEmpty()) { + console.println("No account bridges configured"); + return; + } + + if (!FINGERPRINT.equalsIgnoreCase(args[0])) { + console.println("Unsupported command '" + args[0] + "'"); + printUsage(console); + return; + } + + List handlers; + if (args.length > 1) { + handlers = bridgeHandlers.stream() + .filter(b -> args[1].equalsIgnoreCase(b.getThing().getConfiguration().get("userName").toString())) + .filter(Objects::nonNull).collect(Collectors.toList()); + if (handlers.isEmpty()) { + console.println("No myBMW account bridge for user '" + args[1] + "'"); + printUsage(console); + return; + } + } else { + handlers = bridgeHandlers; + } + + String basePath = FINGERPRINT_ROOT_PATH + File.separator + + LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE); + String path = nextPath(basePath, null); + + console.println("# Start fingerprint"); + int accountNdx = 0; + for (MyBMWBridgeHandler handler : handlers) { + accountNdx++; + console.println("### Account " + String.valueOf(accountNdx)); + if (!ThingStatus.ONLINE.equals(handler.getThing().getStatus())) { + console.println("MyBMW bridge for account not online, cannot create fingerprint"); + } else { + String accountPath = path + File.separator + "Account-" + String.valueOf(accountNdx); + handler.getMyBmwProxy().ifPresentOrElse(prox -> { + // get list of vehicles + List<@NonNull VehicleBase> vehicles = null; + try { + vehicles = prox.requestVehiclesBase(); + + for (String brand : BimmerConstants.REQUESTED_BRANDS) { + console.println("###### Vehicles base for brand " + brand); + printAndSave(console, accountPath, "VehicleBase_" + brand, + prox.requestVehiclesBaseJson(brand)); + } + + if (args.length == 3) { + Optional vehicleOptional = vehicles.stream() + .filter(v -> v.getVin().equalsIgnoreCase(args[2])).findAny(); + if (vehicleOptional.isEmpty()) { + console.println("'" + args[2] + "' is not a valid vin on the account bridge with id '" + + handler.getThing().getUID().getId() + "'"); + printUsage(console); + return; + } + vehicles = List.of(vehicleOptional.get()); + } + + int vinNdx = 0; + for (VehicleBase vehicleBase : vehicles) { + vinNdx++; + String vinPath = accountPath + File.separator + "Vin-" + String.valueOf(vinNdx); + console.println("###### Vehicle " + String.valueOf(vinNdx)); + + // get state + console.println("######## Vehicle state"); + printAndSave(console, vinPath, "VehicleState", prox.requestVehicleStateJson( + vehicleBase.getVin(), vehicleBase.getAttributes().getBrand())); + + // get charge statistics -> only successful for electric vehicles + console.println("######### Vehicle charging statistics"); + printAndSave(console, vinPath, "VehicleChargingStatistics", + prox.requestChargeStatisticsJson(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand())); + + // get charge sessions -> only successful for electric vehicles + console.println("######### Vehicle charging sessions"); + printAndSave(console, vinPath, "VehicleChargingSessions", prox.requestChargeSessionsJson( + vehicleBase.getVin(), vehicleBase.getAttributes().getBrand())); + + console.println("###### End vehicle " + String.valueOf(vinNdx)); + } + } catch (NetworkException e) { + console.println("Fingerprint failed, network exception: " + e.getReason()); + } + }, () -> { + console.println("MyBMW bridge with id '" + handler.getThing().getUID().getId() + + "', communication not started, cannot retrieve fingerprint"); + }); + } + console.println("### End account " + String.valueOf(accountNdx)); + } + + try { + String zipfile = nextPath(basePath, "zip"); + zipDirectory(Paths.get(path), Paths.get(zipfile)); + deleteDirectory(path); + console.println("### Fingerprint has been written to zipfile: " + zipfile); + } catch (IOException e) { + console.println("Exception zipping fingerprint"); + console.println("### Fingerprint has been written to files in directory: " + path); + } + + console.println("# End fingerprint"); + } + + private void printAndSave(Console console, String path, String filename, String content) throws NetworkException { + String json = prettyJson(ResponseContentAnonymizer.anonymizeResponseContent(content)); + console.println(json); + try { + writeJsonToFile(path, filename, json); + } catch (IOException e) { + console.println("Exception writing to file"); + } + } + + private String nextPath(String pathString, @Nullable String extension) { + String path = pathString + ((extension != null) ? ("." + extension) : ""); + int pathNdx = 1; + while (Files.exists(Paths.get(path))) { + path = pathString + "_" + String.valueOf(pathNdx) + ((extension != null) ? ("." + extension) : ""); + pathNdx++; + } + return path; + } + + private String prettyJson(String json) { + try { + return GSON.toJson(JsonParser.parseString(json)); + } catch (JsonSyntaxException e) { + // Keep the unformatted json if there is a syntax exception + return json; + } + } + + private void writeJsonToFile(String pathString, String filename, String json) throws IOException { + try { + JsonElement element = JsonParser.parseString(json); + if (element.isJsonNull() || (element.isJsonArray() && ((JsonArray) element).size() == 0)) { + // Don't write a file if empty + return; + } + } catch (JsonSyntaxException e) { + // Just continue and write the file with non-valid json anyway + } + + String path = nextPath(pathString + File.separator + filename, "json"); + + // ensure full path exists + File file = new File(path); + file.getParentFile().mkdirs(); + + final byte[] contents = json.getBytes(StandardCharsets.UTF_8); + Files.write(file.toPath(), contents); + } + + // Stackoverflow: + // https://stackoverflow.com/questions/57997257/how-can-i-zip-a-complete-directory-with-all-subfolders-in-java + private void zipDirectory(Path sourceDirectoryPath, Path zipPath) throws IOException { + try (FileOutputStream fos = new FileOutputStream(zipPath.toFile()); + ZipOutputStream zos = new ZipOutputStream(fos)) { + Files.walkFileTree(sourceDirectoryPath, new SimpleFileVisitor<@Nullable Path>() { + @Override + public FileVisitResult visitFile(@Nullable Path file, @Nullable BasicFileAttributes attrs) + throws IOException { + zos.putNextEntry(new ZipEntry(sourceDirectoryPath.relativize(file).toString())); + Files.copy(file, zos); + zos.closeEntry(); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw e; + } + } + + private void deleteDirectory(String path) throws IOException { + Files.walk(Paths.get(path)).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + + @Override + public List getUsages() { + return Arrays.asList( + new String[] { buildCommandUsage(FINGERPRINT, "generate fingerprint for all vehicles on all accounts"), + buildCommandUsage(FINGERPRINT + " ", "generate fingerprint for vehicles on account"), + buildCommandUsage(FINGERPRINT + " ", + "generate fingerprint for vehicle with vin on account") }); + } + + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return this; + } + + @Override + public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) { + try { + if (cursorArgumentIndex <= 0) { + return CMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); + } else if (cursorArgumentIndex == 1) { + return new StringsCompleter( + thingRegistry.stream() + .filter(t -> THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(t.getThingTypeUID())) + .map(t -> t.getConfiguration().get("userName").toString()).collect(Collectors.toList()), + false).complete(args, cursorArgumentIndex, cursorPosition, candidates); + } else if (cursorArgumentIndex == 2) { + MyBMWBridgeHandler handler = (MyBMWBridgeHandler) thingRegistry.stream() + .filter(t -> THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(t.getThingTypeUID()) + && args[1].equals(t.getConfiguration().get("userName"))) + .map(t -> t.getHandler()).findAny().get(); + List vehicles = handler.getMyBmwProxy().get().requestVehiclesBase(); + return new StringsCompleter( + vehicles.stream().map(v -> v.getVin()).filter(Objects::nonNull).collect(Collectors.toList()), + false).complete(args, cursorArgumentIndex, cursorPosition, candidates); + } + } catch (NoSuchElementException | NetworkException e) { + return false; + } + return false; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java index c5fa7c36e43e0..e417fa8acb5c8 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java @@ -12,27 +12,28 @@ */ package org.openhab.binding.mybmw.internal.discovery; -import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUPPORTED_THING_SET; - -import java.lang.reflect.Field; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mybmw.internal.MyBMWConstants; import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleAttributes; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleCapabilities; import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler; -import org.openhab.binding.mybmw.internal.handler.RemoteServiceHandler; +import org.openhab.binding.mybmw.internal.handler.backend.MyBMWProxy; +import org.openhab.binding.mybmw.internal.handler.backend.NetworkException; +import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; import org.openhab.binding.mybmw.internal.utils.Constants; import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResultBuilder; -import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerService; @@ -40,130 +41,34 @@ import org.slf4j.LoggerFactory; /** - * The {@link VehicleDiscovery} requests data from BMW API and is identifying the Vehicles after response + * The {@link VehicleDiscovery} requests data from BMW API and is identifying + * the Vehicles after response * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactoring */ @NonNullByDefault -public class VehicleDiscovery extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService { - private static final Logger LOGGER = LoggerFactory.getLogger(VehicleDiscovery.class); - public static final String SUPPORTED_SUFFIX = "Supported"; - public static final String ENABLE_SUFFIX = "Enable"; - public static final String ENABLED_SUFFIX = "Enabled"; - private static final int DISCOVERY_TIMEOUT = 10; - private Optional bridgeHandler = Optional.empty(); +public class VehicleDiscovery extends AbstractDiscoveryService implements ThingHandlerService { - public VehicleDiscovery() { - super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false); - } + private final Logger logger = LoggerFactory.getLogger(VehicleDiscovery.class); - public void onResponse(List vehicleList) { - bridgeHandler.ifPresent(bridge -> { - final ThingUID bridgeUID = bridge.getThing().getUID(); - vehicleList.forEach(vehicle -> { - // the DriveTrain field in the delivered json is defining the Vehicle Type - String vehicleType = VehicleStatusUtils.vehicleType(vehicle.driveTrain, vehicle.model).toString(); - SUPPORTED_THING_SET.forEach(entry -> { - if (entry.getId().equals(vehicleType)) { - ThingUID uid = new ThingUID(entry, vehicle.vin, bridgeUID.getId()); - Map properties = new HashMap<>(); - // Vehicle Properties - properties.put("vehicleModel", vehicle.model); - properties.put("vehicleDriveTrain", vehicle.driveTrain); - properties.put("vehicleConstructionYear", Integer.toString(vehicle.year)); - properties.put("vehicleBodytype", vehicle.bodyType); - - properties.put("servicesSupported", getServices(vehicle, SUPPORTED_SUFFIX, true)); - properties.put("servicesUnsupported", getServices(vehicle, SUPPORTED_SUFFIX, false)); - String servicesEnabled = getServices(vehicle, ENABLED_SUFFIX, true) + Constants.SEMICOLON - + getServices(vehicle, ENABLE_SUFFIX, true); - properties.put("servicesEnabled", servicesEnabled.trim()); - String servicesDisabled = getServices(vehicle, ENABLED_SUFFIX, false) + Constants.SEMICOLON - + getServices(vehicle, ENABLE_SUFFIX, false); - properties.put("servicesDisabled", servicesDisabled.trim()); - - // For RemoteServices we need to do it step-by-step - StringBuffer remoteServicesEnabled = new StringBuffer(); - StringBuffer remoteServicesDisabled = new StringBuffer(); - if (vehicle.capabilities.lock.isEnabled) { - remoteServicesEnabled.append( - RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON); - } else { - remoteServicesDisabled.append( - RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON); - } - if (vehicle.capabilities.unlock.isEnabled) { - remoteServicesEnabled.append( - RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON); - } else { - remoteServicesDisabled.append( - RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON); - } - if (vehicle.capabilities.lights.isEnabled) { - remoteServicesEnabled.append( - RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON); - } else { - remoteServicesDisabled.append( - RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON); - } - if (vehicle.capabilities.horn.isEnabled) { - remoteServicesEnabled.append( - RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON); - } else { - remoteServicesDisabled.append( - RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON); - } - if (vehicle.capabilities.vehicleFinder.isEnabled) { - remoteServicesEnabled.append( - RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON); - } else { - remoteServicesDisabled.append( - RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON); - } - if (vehicle.capabilities.climateNow.isEnabled) { - remoteServicesEnabled.append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel() - + Constants.SEMICOLON); - } else { - remoteServicesDisabled - .append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel() - + Constants.SEMICOLON); - } - properties.put("remoteServicesEnabled", remoteServicesEnabled.toString().trim()); - properties.put("remoteServicesDisabled", remoteServicesDisabled.toString().trim()); - - // Update Properties for already created Things - bridge.getThing().getThings().forEach(vehicleThing -> { - Configuration c = vehicleThing.getConfiguration(); - if (c.containsKey(MyBMWConstants.VIN)) { - String thingVIN = c.get(MyBMWConstants.VIN).toString(); - if (vehicle.vin.equals(thingVIN)) { - vehicleThing.setProperties(properties); - } - } - }); + private static final int DISCOVERY_TIMEOUT = 10; - // Properties needed for functional Thing - properties.put(MyBMWConstants.VIN, vehicle.vin); - properties.put("vehicleBrand", vehicle.brand); - properties.put("refreshInterval", - Integer.toString(MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES)); + private Optional bridgeHandler = Optional.empty(); + private Optional myBMWProxy = Optional.empty(); + private Optional bridgeUid = Optional.empty(); - String vehicleLabel = vehicle.brand + " " + vehicle.model; - Map convertedProperties = new HashMap(properties); - thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID) - .withRepresentationProperty(MyBMWConstants.VIN).withLabel(vehicleLabel) - .withProperties(convertedProperties).build()); - } - }); - }); - }); + public VehicleDiscovery() { + super(MyBMWConstants.SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false); } @Override public void setThingHandler(ThingHandler handler) { if (handler instanceof MyBMWBridgeHandler) { + logger.trace("xxxVehicleDiscovery.setThingHandler for MybmwBridge"); bridgeHandler = Optional.of((MyBMWBridgeHandler) handler); - bridgeHandler.get().setDiscoveryService(this); + bridgeHandler.get().setVehicleDiscovery(this); + bridgeUid = Optional.of(bridgeHandler.get().getThing().getUID()); } } @@ -174,50 +79,150 @@ public void setThingHandler(ThingHandler handler) { @Override protected void startScan() { - bridgeHandler.ifPresent(MyBMWBridgeHandler::requestVehicles); + logger.trace("xxxVehicleDiscovery.startScan"); + discoverVehicles(); } @Override public void deactivate() { + logger.trace("xxxVehicleDiscovery.deactivate"); + super.deactivate(); } - public static String getServices(Vehicle vehicle, String suffix, boolean enabled) { - StringBuffer sb = new StringBuffer(); - List l = getObject(vehicle.capabilities, enabled); - for (String capEntry : l) { - // remove "is" prefix - String cut = capEntry.substring(2); - if (cut.endsWith(suffix)) { - if (sb.length() > 0) { - sb.append(Constants.SEMICOLON); + public void discoverVehicles() { + logger.trace("xxxVehicleDiscovery.discoverVehicles"); + + myBMWProxy = bridgeHandler.get().getMyBmwProxy(); + + try { + Optional> vehicleList = myBMWProxy.map(prox -> { + try { + return prox.requestVehicles(); + } catch (NetworkException e) { + throw new IllegalStateException("vehicles could not be discovered: " + e.getMessage(), e); } - sb.append(cut.substring(0, cut.length() - suffix.length())); - } + }); + vehicleList.ifPresentOrElse(vehicles -> { + bridgeHandler.ifPresent(bridge -> bridge.vehicleDiscoverySuccess()); + processVehicles(vehicles); + }, () -> bridgeHandler.ifPresent(bridge -> bridge.vehicleDiscoveryError())); + } catch (IllegalStateException ex) { + bridgeHandler.ifPresent(bridge -> bridge.vehicleDiscoveryError()); } - return sb.toString(); } /** - * Get all field names from a DTO with a specific value - * Used to get e.g. all services which are "ACTIVATED" - * - * @param DTO Object - * @param compare String which needs to map with the value - * @return String with all field names matching this value separated with Spaces + * this method is called by the bridgeHandler if the list of vehicles was retrieved successfully + * + * @param vehicleList */ - public static List getObject(Object dto, Object compare) { - List l = new ArrayList(); - for (Field field : dto.getClass().getDeclaredFields()) { - try { - Object value = field.get(dto); - if (compare.equals(value)) { - l.add(field.getName()); + private void processVehicles(List vehicleList) { + logger.trace("xxxVehicleDiscovery.processVehicles"); + + vehicleList.forEach(vehicle -> { + // the DriveTrain field in the delivered json is defining the Vehicle Type + String vehicleType = VehicleStatusUtils + .vehicleType(vehicle.getVehicleBase().getAttributes().getDriveTrain(), + vehicle.getVehicleBase().getAttributes().getModel()) + .toString(); + MyBMWConstants.SUPPORTED_THING_SET.forEach(entry -> { + if (entry.getId().equals(vehicleType)) { + ThingUID uid = new ThingUID(entry, vehicle.getVehicleBase().getVin(), bridgeUid.get().getId()); + + Map properties = generateProperties(vehicle); + + boolean thingFound = false; + // Update Properties for already created Things + List vehicleThings = bridgeHandler.get().getThing().getThings(); + for (Thing vehicleThing : vehicleThings) { + Configuration configuration = vehicleThing.getConfiguration(); + // boolean thingFound = true; + if (configuration.containsKey(MyBMWConstants.VIN)) { + String thingVIN = configuration.get(MyBMWConstants.VIN).toString(); + if (vehicle.getVehicleBase().getVin().equals(thingVIN)) { + vehicleThing.setProperties(properties); + thingFound = true; + } + } + } + + if (!thingFound) { + // Properties needed for functional Thing + VehicleAttributes vehicleAttributes = vehicle.getVehicleBase().getAttributes(); + Map convertedProperties = new HashMap(properties); + convertedProperties.put(MyBMWConstants.VIN, vehicle.getVehicleBase().getVin()); + convertedProperties.put("vehicleBrand", vehicleAttributes.getBrand()); + convertedProperties.put("refreshInterval", + Integer.toString(MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES)); + + String vehicleLabel = vehicleAttributes.getBrand() + " " + vehicleAttributes.getModel(); + thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUid.get()) + .withRepresentationProperty(MyBMWConstants.VIN).withLabel(vehicleLabel) + .withProperties(convertedProperties).build()); + } } - } catch (IllegalArgumentException | IllegalAccessException e) { - LOGGER.debug("Field {} not found {}", compare, e.getMessage()); - } + }); + }); + } + + private Map generateProperties(Vehicle vehicle) { + Map properties = new HashMap<>(); + + // Vehicle Properties + VehicleAttributes vehicleAttributes = vehicle.getVehicleBase().getAttributes(); + properties.put("vehicleModel", vehicleAttributes.getModel()); + properties.put("vehicleDriveTrain", vehicleAttributes.getDriveTrain()); + properties.put("vehicleConstructionYear", Integer.toString(vehicleAttributes.getYear())); + properties.put("vehicleBodytype", vehicleAttributes.getBodyType()); + + VehicleCapabilities vehicleCapabilities = vehicle.getVehicleState().getCapabilities(); + + properties.put("servicesSupported", + vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.SUPPORTED_SUFFIX, true)); + properties.put("servicesUnsupported", + vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.SUPPORTED_SUFFIX, false)); + properties.put("servicesEnabled", + vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.ENABLED_SUFFIX, true)); + properties.put("servicesDisabled", + vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.ENABLED_SUFFIX, false)); + + // For RemoteServices we need to do it step-by-step + StringBuffer remoteServicesEnabled = new StringBuffer(); + StringBuffer remoteServicesDisabled = new StringBuffer(); + if (vehicleCapabilities.isLock()) { + remoteServicesEnabled.append(RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON); + } else { + remoteServicesDisabled.append(RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON); } - return l; + if (vehicleCapabilities.isUnlock()) { + remoteServicesEnabled.append(RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON); + } else { + remoteServicesDisabled.append(RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON); + } + if (vehicleCapabilities.isLights()) { + remoteServicesEnabled.append(RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON); + } else { + remoteServicesDisabled.append(RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON); + } + if (vehicleCapabilities.isHorn()) { + remoteServicesEnabled.append(RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON); + } else { + remoteServicesDisabled.append(RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON); + } + if (vehicleCapabilities.isVehicleFinder()) { + remoteServicesEnabled.append(RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON); + } else { + remoteServicesDisabled.append(RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON); + } + if (vehicleCapabilities.isVehicleFinder()) { + remoteServicesEnabled.append(RemoteService.CLIMATE_NOW_START.getLabel() + Constants.SEMICOLON); + } else { + remoteServicesDisabled.append(RemoteService.CLIMATE_NOW_START.getLabel() + Constants.SEMICOLON); + } + properties.put("remoteServicesEnabled", remoteServicesEnabled.toString().trim()); + properties.put("remoteServicesDisabled", remoteServicesDisabled.toString().trim()); + + return properties; } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java index 6896ddb5c305b..0884878fc6bff 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/auth/AuthQueryResponse.java @@ -18,6 +18,7 @@ * The {@link AuthQueryResponse} Data Transfer Object * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - add toString for debugging */ public class AuthQueryResponse { public String clientName;// ": "mybmwapp", @@ -47,4 +48,17 @@ public class AuthQueryResponse { // "authenticate_user" // ], public List promptValues; // ": ["login"] + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return "AuthQueryResponse [clientName=" + clientName + ", clientSecret=" + clientSecret + ", clientId=" + + clientId + ", gcdmBaseUrl=" + gcdmBaseUrl + ", returnUrl=" + returnUrl + ", brand=" + brand + + ", language=" + language + ", country=" + country + ", authorizationEndpoint=" + authorizationEndpoint + + ", tokenEndpoint=" + tokenEndpoint + ", scopes=" + scopes + ", promptValues=" + promptValues + "]"; + } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java deleted file mode 100644 index c8f78dd9a4e80..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeProfile.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.charge; - -import java.util.List; - -/** - * The {@link ChargeProfile} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - edit & send of charge profile - */ -public class ChargeProfile { - public static final Timer INVALID_TIMER = new Timer(); - - public ChargingWindow reductionOfChargeCurrent; - public String chargingMode;// ": "immediateCharging", - public String chargingPreference;// ": "chargingWindow", - public String chargingControlType;// ": "weeklyPlanner", - public List departureTimes; - public boolean climatisationOn;// ": false, - public ChargingSettings chargingSettings; - - public Timer getTimerId(int id) { - if (departureTimes != null) { - for (Timer t : departureTimes) { - if (t.id == id) { - return t; - } - } - } - return INVALID_TIMER; - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java deleted file mode 100644 index f5f97f0c531de..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSession.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.charge; - -/** - * The {@link ChargeSession} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class ChargeSession { - public String id;// ": "2021-12-26T16:57:20Z_128fa4af", - public String title;// ": "Gestern 17:57", - public String subtitle;// ": "Uferstraße 4B • 7h 45min • -- EUR", - public String energyCharged;// ": "~ 31 kWh", - public String sessionStatus;// ": "FINISHED", - public String issues;// ": "2 Probleme", - public String isPublic;// ": false -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java deleted file mode 100644 index eb19b3c5cd368..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessions.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.charge; - -import java.util.List; - -/** - * The {@link ChargeSessions} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class ChargeSessions { - public String total;// ": "~ 218 kWh", - public String numberOfSessions;// ": "17", - public String chargingListState;// ": "HAS_SESSIONS", - public List sessions; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java deleted file mode 100644 index b64318c76adb3..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatistics.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.charge; - -/** - * The {@link ChargeStatistics} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class ChargeStatistics { - public int totalEnergyCharged;// ": 173, - public String totalEnergyChargedSemantics;// ": "Insgesamt circa 173 Kilowattstunden geladen", - public String symbol;// ": "~", - public int numberOfChargingSessions;// ": 13, - public String numberOfChargingSessionsSemantics;// ": "13 Ladevorgänge" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java deleted file mode 100644 index 954b5358b9cf3..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeStatisticsContainer.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.charge; - -/** - * The {@link ChargeStatisticsContainer} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class ChargeStatisticsContainer { - public String description;// ": "Dezember 2021", - public String optStateType;// ": "OPT_IN_WITH_SESSIONS", - public ChargeStatistics statistics;// ": { -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java new file mode 100644 index 0000000000000..668b32f0aa776 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.charge; + +import java.util.ArrayList; +import java.util.List; + +/** + * The {@link ChargingProfile} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + * @author Norbert Truchsess - edit & send of charge profile + * @author Martin Grassl - refactored to Java Bean + */ +public class ChargingProfile { + private ChargingWindow reductionOfChargeCurrent = new ChargingWindow(); + private String chargingMode = "";// ": "immediateCharging", + private String chargingPreference = "";// ": "chargingWindow", + private String chargingControlType = "";// ": "weeklyPlanner", + private List departureTimes = new ArrayList<>(); + private boolean climatisationOn = false;// ": false, + private ChargingSettings chargingSettings = new ChargingSettings(); + + public Timer getTimerId(int id) { + if (departureTimes != null) { + for (Timer t : departureTimes) { + if (t.id == id) { + return t; + } + } + } + return new Timer(); + } + + public ChargingWindow getReductionOfChargeCurrent() { + return reductionOfChargeCurrent; + } + + public String getChargingMode() { + return chargingMode; + } + + public String getChargingPreference() { + return chargingPreference; + } + + public String getChargingControlType() { + return chargingControlType; + } + + public List getDepartureTimes() { + return departureTimes; + } + + public boolean isClimatisationOn() { + return climatisationOn; + } + + public ChargingSettings getChargingSettings() { + return chargingSettings; + } + + @Override + public String toString() { + return "ChargingProfile [reductionOfChargeCurrent=" + reductionOfChargeCurrent + ", chargingMode=" + + chargingMode + ", chargingPreference=" + chargingPreference + ", chargingControlType=" + + chargingControlType + ", departureTimes=" + departureTimes + ", climatisationOn=" + climatisationOn + + ", chargingSettings=" + chargingSettings + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSession.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSession.java new file mode 100644 index 0000000000000..5338c9b62ceef --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSession.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.charge; + +/** + * The {@link ChargingSession} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + */ +public class ChargingSession { + private String id;// ": "2021-12-26T16:57:20Z_128fa4af", + private String title;// ": "Gestern 17:57", + private String subtitle;// ": "Uferstraße 4B • 7h 45min • -- EUR", + private String energyCharged;// ": "~ 31 kWh", + private String sessionStatus;// ": "FINISHED", + private String issues;// ": "2 Probleme", + private String isPublic;// ": false + + /** + * @return the id + */ + public String getId() { + return id; + } + + /** + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * @param title the title to set + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * @return the subtitle + */ + public String getSubtitle() { + return subtitle; + } + + /** + * @return the energyCharged + */ + public String getEnergyCharged() { + return energyCharged; + } + + /** + * @return the sessionStatus + */ + public String getSessionStatus() { + return sessionStatus; + } + + /** + * @return the issues + */ + public String getIssues() { + return issues; + } + + /** + * @return the isPublic + */ + public String getIsPublic() { + return isPublic; + } + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return "ChargingSession [id=" + id + ", title=" + title + ", subtitle=" + subtitle + ", energyCharged=" + + energyCharged + ", sessionStatus=" + sessionStatus + ", issues=" + issues + ", isPublic=" + isPublic + + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessions.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessions.java new file mode 100644 index 0000000000000..258858cf37b08 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessions.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.charge; + +import java.util.List; + +/** + * The {@link ChargingSessions} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + */ +public class ChargingSessions { + private String total;// ": "~ 218 kWh", + private String numberOfSessions;// ": "17", + private String chargingListState;// ": "HAS_SESSIONS", + private List sessions; + + /** + * @return the total + */ + public String getTotal() { + return total; + } + + /** + * @return the numberOfSessions + */ + public String getNumberOfSessions() { + return numberOfSessions; + } + + /** + * @return the chargingListState + */ + public String getChargingListState() { + return chargingListState; + } + + /** + * @return the sessions + */ + public List getSessions() { + return sessions; + } + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return "ChargingSessions [total=" + total + ", numberOfSessions=" + numberOfSessions + ", chargingListState=" + + chargingListState + ", sessions=" + sessions + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessionsContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessionsContainer.java similarity index 78% rename from bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessionsContainer.java rename to bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessionsContainer.java index 93d14e0cf3195..484170411a6ac 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargeSessionsContainer.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessionsContainer.java @@ -13,11 +13,11 @@ package org.openhab.binding.mybmw.internal.dto.charge; /** - * The {@link ChargeSessionsContainer} Data Transfer Object + * The {@link ChargingSessionsContainer} Data Transfer Object * * @author Bernd Weymann - Initial contribution */ -public class ChargeSessionsContainer { +public class ChargingSessionsContainer { public Object paginationInfo; - public ChargeSessions chargingSessions; + public ChargingSessions chargingSessions; } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java index 7a509e2ceb7d9..fc23005cf3177 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSettings.java @@ -16,10 +16,58 @@ * The {@link ChargingSettings} Data Transfer Object * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored to Java Bean */ public class ChargingSettings { - public int targetSoc;// ": 100, - public boolean isAcCurrentLimitActive;// ": false, - public String hospitality;// ": "NO_ACTION", - public String idcc;// ": "NO_ACTION" + private int acCurrentLimit = -1; // 32, + private String hospitality = ""; // HOSP_INACTIVE, + private String idcc = ""; // AUTOMATIC_INTELLIGENT, + private boolean isAcCurrentLimitActive = false; // false, + private int targetSoc = -1; // 80 + + public int getAcCurrentLimit() { + return acCurrentLimit; + } + + public void setAcCurrentLimit(int acCurrentLimit) { + this.acCurrentLimit = acCurrentLimit; + } + + public String getHospitality() { + return hospitality; + } + + public void setHospitality(String hospitality) { + this.hospitality = hospitality; + } + + public String getIdcc() { + return idcc; + } + + public void setIdcc(String idcc) { + this.idcc = idcc; + } + + public boolean isAcCurrentLimitActive() { + return isAcCurrentLimitActive; + } + + public void setAcCurrentLimitActive(boolean isAcCurrentLimitActive) { + this.isAcCurrentLimitActive = isAcCurrentLimitActive; + } + + public int getTargetSoc() { + return targetSoc; + } + + public void setTargetSoc(int targetSoc) { + this.targetSoc = targetSoc; + } + + @Override + public String toString() { + return "ChargingSettings [acCurrentLimit=" + acCurrentLimit + ", hospitality=" + hospitality + ", idcc=" + idcc + + ", isAcCurrentLimitActive=" + isAcCurrentLimitActive + ", targetSoc=" + targetSoc + "]"; + } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatistics.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatistics.java new file mode 100644 index 0000000000000..7816f45fd45b5 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatistics.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.charge; + +/** + * The {@link ChargingStatistics} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactoring + */ +public class ChargingStatistics { + private int totalEnergyCharged;// ": 173, + private String totalEnergyChargedSemantics;// ": "Insgesamt circa 173 Kilowattstunden geladen", + private String symbol;// ": "~", + private int numberOfChargingSessions;// ": 13, + private String numberOfChargingSessionsSemantics;// ": "13 Ladevorgänge" + + /** + * @return the totalEnergyCharged + */ + public int getTotalEnergyCharged() { + return totalEnergyCharged; + } + + /** + * @param totalEnergyCharged the totalEnergyCharged to set + */ + public void setTotalEnergyCharged(int totalEnergyCharged) { + this.totalEnergyCharged = totalEnergyCharged; + } + + /** + * @return the totalEnergyChargedSemantics + */ + public String getTotalEnergyChargedSemantics() { + return totalEnergyChargedSemantics; + } + + /** + * @param totalEnergyChargedSemantics the totalEnergyChargedSemantics to set + */ + public void setTotalEnergyChargedSemantics(String totalEnergyChargedSemantics) { + this.totalEnergyChargedSemantics = totalEnergyChargedSemantics; + } + + /** + * @return the symbol + */ + public String getSymbol() { + return symbol; + } + + /** + * @param symbol the symbol to set + */ + public void setSymbol(String symbol) { + this.symbol = symbol; + } + + /** + * @return the numberOfChargingSessions + */ + public int getNumberOfChargingSessions() { + return numberOfChargingSessions; + } + + /** + * @param numberOfChargingSessions the numberOfChargingSessions to set + */ + public void setNumberOfChargingSessions(int numberOfChargingSessions) { + this.numberOfChargingSessions = numberOfChargingSessions; + } + + /** + * @return the numberOfChargingSessionsSemantics + */ + public String getNumberOfChargingSessionsSemantics() { + return numberOfChargingSessionsSemantics; + } + + /** + * @param numberOfChargingSessionsSemantics the numberOfChargingSessionsSemantics to set + */ + public void setNumberOfChargingSessionsSemantics(String numberOfChargingSessionsSemantics) { + this.numberOfChargingSessionsSemantics = numberOfChargingSessionsSemantics; + } + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return "ChargingStatistics [totalEnergyCharged=" + totalEnergyCharged + ", totalEnergyChargedSemantics=" + + totalEnergyChargedSemantics + ", symbol=" + symbol + ", numberOfChargingSessions=" + + numberOfChargingSessions + ", numberOfChargingSessionsSemantics=" + numberOfChargingSessionsSemantics + + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsContainer.java new file mode 100644 index 0000000000000..1055e131c05c9 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsContainer.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.charge; + +/** + * The {@link ChargingStatisticsContainer} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + */ +public class ChargingStatisticsContainer { + private String description;// ": "Dezember 2021", + private String optStateType;// ": "OPT_IN_WITH_SESSIONS", + private ChargingStatistics statistics;// ": { + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description the description to set + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * @return the optStateType + */ + public String getOptStateType() { + return optStateType; + } + + /** + * @param optStateType the optStateType to set + */ + public void setOptStateType(String optStateType) { + this.optStateType = optStateType; + } + + /** + * @return the statistics + */ + public ChargingStatistics getStatistics() { + return statistics; + } + + /** + * @param statistics the statistics to set + */ + public void setStatistics(ChargingStatistics statistics) { + this.statistics = statistics; + } + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return "ChargingStatisticsContainer [description=" + description + ", optStateType=" + optStateType + + ", statistics=" + statistics + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java index d2d81b8bde56f..f5515cbf43b53 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingWindow.java @@ -16,8 +16,30 @@ * The {@link ChargingWindow} Data Transfer Object * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored to Java Bean */ public class ChargingWindow { - public Time start; - public Time end; + private Time start = new Time(); + private Time end = new Time(); + + public Time getStart() { + return start; + } + + public void setStart(Time start) { + this.start = start; + } + + public Time getEnd() { + return end; + } + + public void setEnd(Time end) { + this.end = end; + } + + @Override + public String toString() { + return "ChargingWindow [start=" + start + ", end=" + end + "]"; + } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/RemoteChargingCommands.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/RemoteChargingCommands.java new file mode 100644 index 0000000000000..1da018f3b67e0 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/RemoteChargingCommands.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.charge; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class RemoteChargingCommands { + private List chargingControl = new ArrayList<>(); + private List flapControl = new ArrayList<>(); + private List plugControl = new ArrayList<>(); + + /** + * @return the chargingControl + */ + public List getChargingControl() { + return chargingControl; + } + + /** + * @param chargingControl the chargingControl to set + */ + public void setChargingControl(List chargingControl) { + this.chargingControl = chargingControl; + } + + /** + * @return the flapControl + */ + public List getFlapControl() { + return flapControl; + } + + /** + * @param flapControl the flapControl to set + */ + public void setFlapControl(List flapControl) { + this.flapControl = flapControl; + } + + /** + * @return the plugControl + */ + public List getPlugControl() { + return plugControl; + } + + /** + * @param plugControl the plugControl to set + */ + public void setPlugControl(List plugControl) { + this.plugControl = plugControl; + } + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return "RemoteChargingCommands [chargingControl=" + chargingControl + ", flapControl=" + flapControl + + ", plugControl=" + plugControl + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java index 6bc212fc61bcc..64ff1e3f4643e 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java @@ -12,20 +12,35 @@ */ package org.openhab.binding.mybmw.internal.dto.charge; -import org.openhab.binding.mybmw.internal.utils.Converter; - /** * The {@link Time} Data Transfer Object * * @author Bernd Weymann - Initial contribution * @author Norbert Truchsess - edit & send of charge profile + * @author Martin Grassl - refactored to Java Bean */ public class Time { - public int hour;// ": 11, - public int minute;// ": 0 + private int hour = -1;// ": 11, + private int minute = -1;// ": 0 + + public int getHour() { + return hour; + } + + public void setHour(int hour) { + this.hour = hour; + } + + public int getMinute() { + return minute; + } + + public void setMinute(int minute) { + this.minute = minute; + } @Override public String toString() { - return Converter.getTime(this); + return "Time [hour=" + hour + ", minute=" + minute + "]"; } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java deleted file mode 100644 index 48d2b74fdc634..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/network/NetworkError.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.network; - -import org.openhab.binding.mybmw.internal.utils.Constants; -import org.openhab.binding.mybmw.internal.utils.Converter; - -/** - * The {@link NetworkError} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class NetworkError { - public String url; - public int status; - public String reason; - public String params; - - @Override - public String toString() { - return new StringBuilder(url).append(Constants.HYPHEN).append(status).append(Constants.HYPHEN).append(reason) - .append(params).toString(); - } - - public String toJson() { - return Converter.getGson().toJson(this); - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java deleted file mode 100644 index 1f11ce60f7837..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CBS.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -import org.openhab.binding.mybmw.internal.utils.Constants; - -/** - * The {@link CBS} Data Transfer Object ConditionBasedService - * - * @author Bernd Weymann - Initial contribution - */ -public class CBS { - public String type = Constants.NO_ENTRIES;// ": "BRAKE_FLUID", - public String status = Constants.NO_ENTRIES;// ": "OK", - public String dateTime;// ": "2023-11-01T00:00:00.000Z" - public Distance distance; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java deleted file mode 100644 index 78dd306d6a88c..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/CCM.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link CCM} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class CCM { - // [todo] [todo] definition currently unknown -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java deleted file mode 100644 index 501a80851ed6c..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/ChargingState.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link ChargingState} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class ChargingState { - public int chargePercentage;// ": 74, - public String state;// ": "NOT_CHARGING", - public String type;// ": "NOT_AVAILABLE", - public boolean isChargerConnected;// ": false -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java deleted file mode 100644 index 655483b1d8244..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Coordinates.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link Coordinates} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Coordinates { - public double latitude; - public double longitude; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java deleted file mode 100644 index 9a169ad78068b..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Distance.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link Distance} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Distance { - public int value;// ": 31, - public String units;// ": "KILOMETERS" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java deleted file mode 100644 index 1f1a5c62531d2..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Doors.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link Doors} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Doors { - public String driverFront;// ": "CLOSED", - public String driverRear;// ": "CLOSED", - public String passengerFront;// ": "CLOSED", - public String passengerRear;// ": "CLOSED" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java deleted file mode 100644 index 7456e993b92ce..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/DoorsWindows.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link DoorsWindows} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class DoorsWindows { - public Doors doors; - public Windows windows; - public String trunk;// ": "CLOSED", - public String hood;// ": "CLOSED", - public String moonroof;// ": "CLOSED" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java deleted file mode 100644 index 3ec39335f899b..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/FuelLevel.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link FuelLevel} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class FuelLevel { - public int value;// ": 4, - public String units;// ": "LITERS" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java deleted file mode 100644 index d7e71afde191d..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Location.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link Location} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Location { - public Coordinates coordinates; - public Address address; - public int heading; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java deleted file mode 100644 index 025f87fe0beca..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Properties.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -import java.util.List; - -/** - * The {@link Properties} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Properties { - public String lastUpdatedAt;// ": "2021-12-21T16:46:02Z", - public boolean inMotion;// ": false, - public boolean areDoorsLocked;// ": true, - public String originCountryISO;// ": "DE", - public boolean areDoorsClosed;// ": true, - public boolean areDoorsOpen;// ": false, - public boolean areWindowsClosed;// ": true, - public DoorsWindows doorsAndWindows;// ": - public boolean isServiceRequired;// ":false - public FuelLevel fuelLevel; - public ChargingState chargingState;// ": - public Range combustionRange; - public Range combinedRange; - public Range electricRange; - public Range electricRangeAndStatus; - public List checkControlMessages; - public List serviceRequired; - public Location vehicleLocation; - public Tires tires; - // "climateControl":{} [todo] definition currently unknown -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java deleted file mode 100644 index 32ed8072a63bd..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Range.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link Range} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Range { - public int chargePercentage; - public Distance distance; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java deleted file mode 100644 index 678ff17648e02..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tire.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link Tire} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Tire { - public TireStatus status; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java deleted file mode 100644 index 763b693c585e2..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/TireStatus.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link TireStatus} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class TireStatus { - public double currentPressure;// ": 220, - public String localizedCurrentPressure;// ": "2.2 bar", - public String localizedTargetPressure;// ": "2.3 bar", - public double targetPressure;// ": 230 -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java deleted file mode 100644 index c956a769f4b17..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Tires.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link Tires} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Tires { - public Tire frontLeft; - public Tire frontRight; - public Tire rearLeft; - public Tire rearRight; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java deleted file mode 100644 index 1b08344e60bfe..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Windows.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.properties; - -/** - * The {@link Windows} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Windows { - public String driverFront;// ": "CLOSED", - public String driverRear;// ": "CLOSED", - public String passengerFront;// ": "CLOSED", - public String passengerRear;// ": "CLOSED" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java index f37dcf08711b0..38ed251b442ed 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionError.java @@ -22,7 +22,7 @@ public class ExecutionError { public String description;// ": "Die folgenden Einschränkungen verbieten die Ausführung von Remote Services: Aus // Sicherheitsgründen sind Remote Services nicht verfügbar, wenn die Fahrbereitschaft // eingeschaltet ist. Remote Services können nur mit einem ausreichenden Ladezustand - // durchgeführt werden. Die Remote Services „Verriegeln“ und „Entriegeln“ können nur + // durchgeführt werden. Die Remote Services „Verriegeln" und „Entriegeln" können nur // ausgeführt werden, wenn die Fahrertür geschlossen und der Türstatus bekannt ist.", public String presentationType;// ": "PAGE", public int iconId;// ": 60217, diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java index 58b7c8ed27a7a..0ab6d33592dba 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/remote/ExecutionStatusContainer.java @@ -16,10 +16,49 @@ * The {@link ExecutionStatusContainer} Data Transfer Object * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored to Java Bean */ public class ExecutionStatusContainer { - public String eventId; - public String creationTime; - public String eventStatus; - public ExecutionError errorDetails; + private String eventId = ""; + private String creationTime = ""; + private String eventStatus = ""; + private ExecutionError errorDetails = null; + + public String getEventId() { + return eventId; + } + + public void setEventId(String eventId) { + this.eventId = eventId; + } + + public String getCreationTime() { + return creationTime; + } + + public void setCreationTime(String creationTime) { + this.creationTime = creationTime; + } + + public String getEventStatus() { + return eventStatus; + } + + public void setEventStatus(String eventStatus) { + this.eventStatus = eventStatus; + } + + public ExecutionError getErrorDetails() { + return errorDetails; + } + + public void setErrorDetails(ExecutionError errorDetails) { + this.errorDetails = errorDetails; + } + + @Override + public String toString() { + return "ExecutionStatusContainer [eventId=" + eventId + ", creationTime=" + creationTime + ", eventStatus=" + + eventStatus + ", errorDetails=" + errorDetails + "]"; + } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java deleted file mode 100644 index de2f51660ae03..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CBSMessage.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.status; - -/** - * The {@link CBSMessage} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class CBSMessage { - public String id;// ": "BrakeFluid", - public String title;// ": "Brake fluid", - public int iconId;// ": 60223, - public String longDescription;// ": "Next service due by the specified date.", - public String subtitle;// ": "Due in November 2023", - public String criticalness;// ": "nonCritical" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java deleted file mode 100644 index dc99ad4d7392d..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/CCMMessage.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.status; - -import org.openhab.binding.mybmw.internal.utils.Constants; - -/** - * The {@link CCMMessage} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class CCMMessage { - public String criticalness;// ": "semiCritical", - public int iconId;// ": 60217, - public String state = Constants.NO_ENTRIES;// ": "Medium", - public String title = Constants.NO_ENTRIES;// ": "Battery discharged: Start engine" - public String id;// ": "229", - public String longDescription = Constants.NO_ENTRIES;// ": "Charge by driving for longer periods or use external - // charger. Functions requiring battery will be switched off. -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java deleted file mode 100644 index 7994c459f77c0..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/DoorWindow.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.status; - -/** - * The {@link DoorWindow} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class DoorWindow { - public int iconId;// ": 59757, - public String title;// ": "Lock status", - public String state;// ": "Locked", - public String criticalness;// ": "nonCritical" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java deleted file mode 100644 index ff44f62882c4e..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/FuelIndicator.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.status; - -/** - * The {@link FuelIndicator} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class FuelIndicator { - public int mainBarValue;// ": 74, - public String rangeUnits;// ": "km", - public String rangeValue;// ": "76", - public String levelUnits;// ": "%", - public String levelValue;// ": "74", - - public int secondaryBarValue;// ": 0, - public int infoIconId;// ": 59694, - public int rangeIconId;// ": 59683, - public int levelIconId;// ": 59694, - public boolean showsBar;// ": true, - public boolean showBarGoal;// ": false, - public String barType;// ": null, - public String infoLabel;// ": "State of Charge", - public boolean isInaccurate;// ": false, - public boolean isCircleIcon;// ": false, - public String iconOpacity;// ": "high", - public String chargingType;// ": null, - public String chargingStatusType;// ": "DEFAULT", - public String chargingStatusIndicatorType;// ": "DEFAULT" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java deleted file mode 100644 index d2dff9c0f992e..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Issues.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.status; - -/** - * The {@link Issues} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Issues { - // [todo] definition currently unknown -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java deleted file mode 100644 index c1f340771cfad..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Mileage.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.status; - -/** - * The {@link Mileage} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Mileage { - public int mileage;// ": 31537, - public String units;// ": "km", - public String formattedMileage;// ": "31537" -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java deleted file mode 100644 index 37b47f1751de7..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/status/Status.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.status; - -import java.util.List; - -import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile; - -/** - * The {@link Status} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ -public class Status { - public String lastUpdatedAt;// ": "2021-12-21T16:46:02Z", - public Mileage currentMileage; - public Issues issues; - public String doorsGeneralState;// ":"Locked", - public String checkControlMessagesGeneralState;// ":"No Issues", - public List doorsAndWindows;// ":[ - public List checkControlMessages;// - public List requiredServices;// - // "recallMessages":[], - // "recallExternalUrl":null, - public List fuelIndicators; - public String timestampMessage;// ":"Updated from vehicle 12/21/2021 05:46 PM", - public ChargeProfile chargingProfile; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Address.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Address.java similarity index 61% rename from bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Address.java rename to bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Address.java index 7041993ca1be8..7ab723ec4a9ef 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/properties/Address.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Address.java @@ -10,13 +10,23 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mybmw.internal.dto.properties; +package org.openhab.binding.mybmw.internal.dto.vehicle; /** * The {@link Address} Data Transfer Object * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored to Java Bean */ public class Address { - public String formatted; + private String formatted = ""; + + public String getFormatted() { + return formatted; + } + + @Override + public String toString() { + return "Address [formatted=" + formatted + "]"; + } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java deleted file mode 100644 index ef5a4289a6608..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Capabilities.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto.vehicle; - -/** - * The {@link Capabilities} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution - */ - -public class Capabilities { - public boolean isRemoteServicesBookingRequired; - public boolean isRemoteServicesActivationRequired; - public boolean isRemoteHistorySupported; - public boolean canRemoteHistoryBeDeleted; - public boolean isChargingHistorySupported; - public boolean isScanAndChargeSupported; - public boolean isDCSContractManagementSupported; - public boolean isBmwChargingSupported; - public boolean isMiniChargingSupported; - public boolean isChargeNowForBusinessSupported; - public boolean isDataPrivacyEnabled; - public boolean isChargingPlanSupported; - public boolean isChargingPowerLimitEnable; - public boolean isChargingTargetSocEnable; - public boolean isChargingLoudnessEnable; - public boolean isChargingSettingsEnabled; - public boolean isChargingHospitalityEnabled; - public boolean isEvGoChargingSupported; - public boolean isFindChargingEnabled; - public boolean isCustomerEsimSupported; - public boolean isCarSharingSupported; - public boolean isEasyChargeSupported; - - public RemoteService lock; - public RemoteService unlock; - public RemoteService lights; - public RemoteService horn; - public RemoteService vehicleFinder; - public RemoteService sendPoi; - public RemoteService climateNow; -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CheckControlMessage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CheckControlMessage.java new file mode 100644 index 0000000000000..b6263fa34d55d --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CheckControlMessage.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class CheckControlMessage { + private String type = ""; // TIRE_PRESSURE, + private String severity = ""; // LOW + private int id = -1; // 955, + private String description = ""; // Tire pressure notification: You can continue driving. Check tire pressure when + // the tires are cold and adjust if necessary. Perform reset after adjustment. See + // Owner's Manual for further information. + private String name = ""; // Tire pressure notification + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "CheckControlMessage [type=" + type + ", severity=" + severity + ", id=" + id + ", description=" + + description + ", name=" + name + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RemoteService.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateControlState.java similarity index 50% rename from bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RemoteService.java rename to bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateControlState.java index 3551a70c58274..051da6da08b70 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RemoteService.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateControlState.java @@ -13,12 +13,24 @@ package org.openhab.binding.mybmw.internal.dto.vehicle; /** - * The {@link RemoteService} Data Transfer Object - * - * @author Bernd Weymann - Initial contribution + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution */ -public class RemoteService { - public boolean isEnabled;// ": true, - public boolean isPinAuthenticationRequired;// ": false, - public String executionMessage;// ": "Lock your vehicle now? Remote functions may take a few seconds." +public class ClimateControlState { + private String activity = ""; // INACTIVE + + public String getActivity() { + return activity; + } + + public void setActivity(String activity) { + this.activity = activity; + } + + @Override + public String toString() { + return "ClimateControlState [activity=" + activity + "]"; + } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateTimer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateTimer.java new file mode 100644 index 0000000000000..1a0aeb7378641 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateTimer.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class ClimateTimer { + private boolean isWeeklyTimer = false; // true, + private String timerAction = ""; // DEACTIVATE, + private List timerWeekDays = new ArrayList<>(); // [ MONDAY ] + private DepartureTime departureTime = new DepartureTime(); + + public boolean isWeeklyTimer() { + return isWeeklyTimer; + } + + public void setWeeklyTimer(boolean isWeeklyTimer) { + this.isWeeklyTimer = isWeeklyTimer; + } + + public String getTimerAction() { + return timerAction; + } + + public void setTimerAction(String timerAction) { + this.timerAction = timerAction; + } + + public List getTimerWeekDays() { + return timerWeekDays; + } + + public void setTimerWeekDays(List timerWeekDays) { + this.timerWeekDays = timerWeekDays; + } + + public DepartureTime getDepartureTime() { + return departureTime; + } + + public void setDepartureTime(DepartureTime departureTime) { + this.departureTime = departureTime; + } + + @Override + public String toString() { + return "ClimateTimer [isWeeklyTimer=" + isWeeklyTimer + ", timerAction=" + timerAction + ", timerWeekDays=" + + timerWeekDays + ", departureTime=" + departureTime + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CombustionFuelLevel.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CombustionFuelLevel.java new file mode 100644 index 0000000000000..46424f2f5a594 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CombustionFuelLevel.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class CombustionFuelLevel { + private int remainingFuelPercent = -1; // 65, + private int remainingFuelLiters = -1; // 34, + private int range = -1; // 435 + + public int getRemainingFuelPercent() { + return remainingFuelPercent; + } + + public void setRemainingFuelPercent(int remainingFuelPercent) { + this.remainingFuelPercent = remainingFuelPercent; + } + + public int getRemainingFuelLiters() { + return remainingFuelLiters; + } + + public void setRemainingFuelLiters(int remainingFuelLiters) { + this.remainingFuelLiters = remainingFuelLiters; + } + + public int getRange() { + return range; + } + + public void setRange(int range) { + this.range = range; + } + + @Override + public String toString() { + return "CombustionFuelLevel [remainingFuelPercent=" + remainingFuelPercent + ", remainingFuelLiters=" + + remainingFuelLiters + ", range=" + range + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Coordinates.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Coordinates.java new file mode 100644 index 0000000000000..ca4912fb9c968 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Coordinates.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * The {@link Coordinates} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored to Java Bean + */ +public class Coordinates { + private double latitude = -1.0; + private double longitude = -1.0; + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + @Override + public String toString() { + return "Coordinates [latitude=" + latitude + ", longitude=" + longitude + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DepartureTime.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DepartureTime.java new file mode 100644 index 0000000000000..a9a16480def4f --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DepartureTime.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class DepartureTime { + private int hour = -1; // 7, + private int minute = -1; // 0 + + public int getHour() { + return hour; + } + + public void setHour(int hour) { + this.hour = hour; + } + + public int getMinute() { + return minute; + } + + public void setMinute(int minute) { + this.minute = minute; + } + + @Override + public String toString() { + return "DepartureTime [hour=" + hour + ", minute=" + minute + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DigitalKey.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DigitalKey.java new file mode 100644 index 0000000000000..c639efab68939 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DigitalKey.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class DigitalKey { + private String bookedServicePackage = ""; // NONE, + private String readerGraphics = ""; + private String state = ""; // NOT_AVAILABLE + + public String getBookedServicePackage() { + return bookedServicePackage; + } + + public void setBookedServicePackage(String bookedServicePackage) { + this.bookedServicePackage = bookedServicePackage; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getReaderGraphics() { + return readerGraphics; + } + + public void setReaderGraphics(String readerGraphics) { + this.readerGraphics = readerGraphics; + } + + @Override + public String toString() { + return "DigitalKey [bookedServicePackage=" + bookedServicePackage + ", readerGraphics=" + readerGraphics + + ", state=" + state + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DriverPreferences.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DriverPreferences.java new file mode 100644 index 0000000000000..8ae43c4eb4175 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DriverPreferences.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class DriverPreferences { + private String lscPrivacyMode = ""; // OFF + + public String getLscPrivacyMode() { + return lscPrivacyMode; + } + + public void setLscPrivacyMode(String lscPrivacyMode) { + this.lscPrivacyMode = lscPrivacyMode; + } + + @Override + public String toString() { + return "DriverPreferences [lscPrivacyMode=" + lscPrivacyMode + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ElectricChargingState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ElectricChargingState.java new file mode 100644 index 0000000000000..9706655f18d6a --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ElectricChargingState.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + * @author Mark Herwege - refactoring, V2 API charging + */ +public class ElectricChargingState { + private String chargingConnectionType = ""; // UNKNOWN, + private String chargingStatus = ""; // FINISHED_FULLY_CHARGED, + private boolean isChargerConnected = false; // true, + private int chargingTarget = -1; // 80, + private int chargingLevelPercent = -1; // 80, + private int remainingChargingMinutes = -1; // 178 + private int range = -1; // 286 + + /** + * @return the chargingConnectionType + */ + public String getChargingConnectionType() { + return chargingConnectionType; + } + + /** + * @param chargingConnectionType the chargingConnectionType to set + */ + public void setChargingConnectionType(String chargingConnectionType) { + this.chargingConnectionType = chargingConnectionType; + } + + /** + * @return the chargingStatus + */ + public String getChargingStatus() { + return chargingStatus; + } + + /** + * @param chargingStatus the chargingStatus to set + */ + public void setChargingStatus(String chargingStatus) { + this.chargingStatus = chargingStatus; + } + + /** + * @return the isChargerConnected + */ + public boolean isChargerConnected() { + return isChargerConnected; + } + + /** + * @param isChargerConnected the isChargerConnected to set + */ + public void setChargerConnected(boolean isChargerConnected) { + this.isChargerConnected = isChargerConnected; + } + + /** + * @return the chargingTarget + */ + public int getChargingTarget() { + return chargingTarget; + } + + /** + * @param chargingTarget the chargingTarget to set + */ + public void setChargingTarget(int chargingTarget) { + this.chargingTarget = chargingTarget; + } + + /** + * @return the chargingLevelPercent + */ + public int getChargingLevelPercent() { + return chargingLevelPercent; + } + + /** + * @param chargingLevelPercent the chargingLevelPercent to set + */ + public void setChargingLevelPercent(int chargingLevelPercent) { + this.chargingLevelPercent = chargingLevelPercent; + } + + /** + * @return the remainingChargingMinutes + */ + public int getRemainingChargingMinutes() { + return remainingChargingMinutes; + } + + /** + * @param remainingChargingMinutes the remainingChargingMinutes to set + */ + public void setRemainingChargingMinutes(int remainingChargingMinutes) { + this.remainingChargingMinutes = remainingChargingMinutes; + } + + /** + * @return the range + */ + public int getRange() { + return range; + } + + /** + * @param range the range to set + */ + public void setRange(int range) { + this.range = range; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return "ElectricChargingState [chargingConnectionType=" + chargingConnectionType + ", chargingStatus=" + + chargingStatus + ", isChargerConnected=" + isChargerConnected + ", chargingTarget=" + chargingTarget + + ", chargingLevelPercent=" + chargingLevelPercent + ", remainingChargingMinutes=" + + remainingChargingMinutes + ", range=" + range + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RequiredService.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RequiredService.java new file mode 100644 index 0000000000000..7c4acea4dc4c6 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RequiredService.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class RequiredService { + private String dateTime = ""; // 2024-06-01T00:00:00.000Z, + private int mileage = -1; // 29000, + private String type = ""; // OIL, + private String status = ""; // OK, + private String description = ""; // Next service due after the specified distance or date. + + public String getDateTime() { + return dateTime; + } + + public void setDateTime(String dateTime) { + this.dateTime = dateTime; + } + + public int getMileage() { + return mileage; + } + + public void setMileage(int mileage) { + this.mileage = mileage; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String toString() { + return "RequiredService [dateTime=" + dateTime + ", mileage=" + mileage + ", type=" + type + ", status=" + + status + ", description=" + description + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java index 9392527f0fa14..f6fd2a1a51cfe 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Vehicle.java @@ -12,35 +12,34 @@ */ package org.openhab.binding.mybmw.internal.dto.vehicle; -import org.openhab.binding.mybmw.internal.dto.properties.Properties; -import org.openhab.binding.mybmw.internal.dto.status.Status; - /** * The {@link Vehicle} Data Transfer Object * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored for v2 API */ public class Vehicle { - public String vin;// ": "WBY1Z81040V905639", - public String model;// ": "i3 94 (+ REX)", - public int year;// ": 2017, - public String brand;// ": "BMW", - public String headUnit;// ": "ID5", - public boolean isLscSupported;// ": true, - public String driveTrain;// ": "ELECTRIC", - public String puStep;// ": "0321", - public String iStep;// ": "I001-21-03-530", - public String telematicsUnit;// ": "TCB1", - public String hmiVersion;// ": "ID4", - public String bodyType;// ": "I01", - public String a4aType;// ": "USB_ONLY", - public String exFactoryPUStep;// ": "0717", - public String exFactoryILevel;// ": "I001-17-07-500" - public Capabilities capabilities; - // "connectedDriveServices": [] currently no clue how to resolve, - public Properties properties; - public boolean isMappingPending;// ":false," - public boolean isMappingUnconfirmed;// ":false, - public Status status; - public boolean valid = false; + private VehicleBase vehicleBase = new VehicleBase(); + private VehicleStateContainer vehicleState = new VehicleStateContainer(); + + public VehicleBase getVehicleBase() { + return vehicleBase; + } + + public void setVehicleBase(VehicleBase vehicleBase) { + this.vehicleBase = vehicleBase; + } + + public VehicleStateContainer getVehicleState() { + return vehicleState; + } + + public void setVehicleState(VehicleStateContainer vehicleState) { + this.vehicleState = vehicleState; + } + + @Override + public String toString() { + return "Vehicle [vehicleBase=" + vehicleBase + ", vehicleState=" + vehicleState + "]"; + } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleAttributes.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleAttributes.java new file mode 100644 index 0000000000000..882a01f34ff02 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleAttributes.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +import org.openhab.binding.mybmw.internal.utils.BimmerConstants; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + * @author Mark Herwege - fix brand BMW_I + */ +public class VehicleAttributes { + private String lastFetched = ""; // "2022-12-21T17:30:40.363Z" + private String model = "";// ": "i3 94 (+ REX)", + private int year = -1;// ": 2017, + private long color = -1;// ": 4284572001, + private String brand = "";// ": "BMW", + private String driveTrain = "";// ": "ELECTRIC", + private String headUnitType = "";// ": "ID5", + private String headUnitRaw = "";// ": "ID5", + private String hmiVersion = "";// ": "ID4", + // softwareVersionCurrent - needed? + // softwareVersionExFactory - needed? + private String telematicsUnit = "";// ": "TCB1", + private String bodyType = "";// ": "I01", + private String countryOfOrigin = ""; // "DE" + // driverGuideInfo - needed? + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public long getColor() { + return color; + } + + public void setColor(long color) { + this.color = color; + } + + public String getBrand() { + if (BimmerConstants.BRAND_BMWI.equals(brand.toLowerCase())) { + return BimmerConstants.BRAND_BMW; + } else { + return brand.toLowerCase(); + } + } + + public void setBrand(String brand) { + this.brand = brand; + } + + public String getDriveTrain() { + return driveTrain; + } + + public void setDriveTrain(String driveTrain) { + this.driveTrain = driveTrain; + } + + public String getHeadUnitType() { + return headUnitType; + } + + public void setHeadUnitType(String headUnitType) { + this.headUnitType = headUnitType; + } + + public String getHeadUnitRaw() { + return headUnitRaw; + } + + public void setHeadUnitRaw(String headUnitRaw) { + this.headUnitRaw = headUnitRaw; + } + + public String getHmiVersion() { + return hmiVersion; + } + + public void setHmiVersion(String hmiVersion) { + this.hmiVersion = hmiVersion; + } + + public String getTelematicsUnit() { + return telematicsUnit; + } + + public void setTelematicsUnit(String telematicsUnit) { + this.telematicsUnit = telematicsUnit; + } + + public String getBodyType() { + return bodyType; + } + + public void setBodyType(String bodyType) { + this.bodyType = bodyType; + } + + public String getCountryOfOrigin() { + return countryOfOrigin; + } + + public void setCountryOfOrigin(String countryOfOrigin) { + this.countryOfOrigin = countryOfOrigin; + } + + public String getLastFetched() { + return lastFetched; + } + + public void setLastFetched(String lastFetched) { + this.lastFetched = lastFetched; + } + + @Override + public String toString() { + return "VehicleAttributes [lastFetched=" + lastFetched + ", model=" + model + ", year=" + year + ", color=" + + color + ", brand=" + brand + ", driveTrain=" + driveTrain + ", headUnitType=" + headUnitType + + ", headUnitRaw=" + headUnitRaw + ", hmiVersion=" + hmiVersion + ", telematicsUnit=" + telematicsUnit + + ", bodyType=" + bodyType + ", countryOfOrigin=" + countryOfOrigin + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBase.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBase.java new file mode 100644 index 0000000000000..b8dadbad1e506 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBase.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * The {@link VehicleBase} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored to Java Bean + */ +public class VehicleBase { + private String vin = "";// ": "WBY1Z81040V905639", + // mappingInfo - needed? + // appVehicleType - needed? + private VehicleAttributes attributes = new VehicleAttributes(); + + public String getVin() { + return vin; + } + + public void setVin(String vin) { + this.vin = vin; + } + + public VehicleAttributes getAttributes() { + return attributes; + } + + public void setAttributes(VehicleAttributes attributes) { + this.attributes = attributes; + } + + @Override + public String toString() { + return "VehicleBase [vin=" + vin + ", attributes=" + attributes + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java new file mode 100644 index 0000000000000..d58eb4eefde88 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java @@ -0,0 +1,233 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.openhab.binding.mybmw.internal.dto.charge.RemoteChargingCommands; +import org.openhab.binding.mybmw.internal.utils.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VehicleCapabilities} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored to Java Bean + */ + +public class VehicleCapabilities { + private final Logger logger = LoggerFactory.getLogger(VehicleCapabilities.class); + + private static final String PREFIX_IS = "is"; + public static final String SUPPORTED_SUFFIX = "Supported"; + public static final String ENABLED_SUFFIX = "Enabled"; + + // private boolean remoteChargingCommands = false; // {}, don't know what comes + // private boolean specialThemeSupport = false; // [] don't know what comes here + private boolean checkSustainabilityDPP = false; // false, + private boolean climateNow = false; // true, + private boolean horn = false; // true, + private boolean isBmwChargingSupported = false; // false, + private boolean isCarSharingSupported = false; // false, + private boolean isChargeNowForBusinessSupported = false; // false, + private boolean isChargingHistorySupported = false; // false, + private boolean isChargingHospitalityEnabled = false; // false, + private boolean isChargingLoudnessEnabled = false; // false, + private boolean isChargingPlanSupported = false; // false, + private boolean isChargingPowerLimitEnabled = false; // false, + private boolean isChargingSettingsEnabled = false; // false, + private boolean isChargingTargetSocEnabled = false; // false, + private boolean isClimateTimerSupported = false; // true, + private boolean isClimateTimerWeeklyActive = false; // true, + private boolean isCustomerEsimSupported = false; // false, + private boolean isDataPrivacyEnabled = false; // false, + private boolean isDCSContractManagementSupported = false; // false, + private boolean isEasyChargeEnabled = false; // false, + private boolean isEvGoChargingSupported = false; // false, + private boolean isMiniChargingSupported = false; // false, + private boolean isNonLscFeatureEnabled = false; // false, + private boolean isRemoteEngineStartSupported = false; // false, + private boolean isRemoteHistoryDeletionSupported = false; // false, + private boolean isRemoteHistorySupported = false; // true, + private boolean isRemoteParkingSupported = false; // false, + private boolean isRemoteServicesActivationRequired = false; // false, + private boolean isRemoteServicesBookingRequired = false; // false, + private boolean isScanAndChargeSupported = false; // false, + private boolean isSustainabilityAccumulatedViewEnabled = false; // false, + private boolean isSustainabilitySupported = false; // false, + private boolean isWifiHotspotServiceSupported = false; // true, + private boolean lights = false; // true, + private boolean lock = false; // true, + private boolean remote360 = false; + private RemoteChargingCommands remoteChargingCommands = new RemoteChargingCommands(); + private boolean remoteSoftwareUpgrade = false; // true, + private boolean sendPoi = false; // true, + private boolean speechThirdPartyAlexa = false; // true, + private boolean speechThirdPartyAlexaSDK = false; // false, + private boolean unlock = false; // true, + + /** + * @return the climateNow + */ + public boolean isClimateNow() { + return climateNow; + } + + /** + * @return the horn + */ + public boolean isHorn() { + return horn; + } + + /** + * @return the lights + */ + public boolean isLights() { + return lights; + } + + /** + * @return the lock + */ + public boolean isLock() { + return lock; + } + + /** + * @return the remote360 + */ + public boolean isRemote360() { + return remote360; + } + + /** + * @return the sendPoi + */ + public boolean isSendPoi() { + return sendPoi; + } + + /** + * @return the unlock + */ + public boolean isUnlock() { + return unlock; + } + + /** + * @return the vehicleFinder + */ + public boolean isVehicleFinder() { + return vehicleFinder; + } + + /** + * @return the digitalKey + */ + public DigitalKey getDigitalKey() { + return digitalKey; + } + + private boolean vehicleFinder = false; // true, + private DigitalKey digitalKey = new DigitalKey(); + private String a4aType = ""; // NOT_SUPPORTED, + private String climateFunction = ""; // VENTILATION, + private String climateTimerTrigger = ""; // DEPARTURE_TIMER, + private String lastStateCallState = ""; // ACTIVATED, + private String vehicleStateSource = ""; // LAST_STATE_CALL, + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return "VehicleCapabilities [checkSustainabilityDPP=" + checkSustainabilityDPP + ", climateNow=" + climateNow + + ", horn=" + horn + ", isBmwChargingSupported=" + isBmwChargingSupported + ", isCarSharingSupported=" + + isCarSharingSupported + ", isChargeNowForBusinessSupported=" + isChargeNowForBusinessSupported + + ", isChargingHistorySupported=" + isChargingHistorySupported + ", isChargingHospitalityEnabled=" + + isChargingHospitalityEnabled + ", isChargingLoudnessEnabled=" + isChargingLoudnessEnabled + + ", isChargingPlanSupported=" + isChargingPlanSupported + ", isChargingPowerLimitEnabled=" + + isChargingPowerLimitEnabled + ", isChargingSettingsEnabled=" + isChargingSettingsEnabled + + ", isChargingTargetSocEnabled=" + isChargingTargetSocEnabled + ", isClimateTimerSupported=" + + isClimateTimerSupported + ", isClimateTimerWeeklyActive=" + isClimateTimerWeeklyActive + + ", isCustomerEsimSupported=" + isCustomerEsimSupported + ", isDataPrivacyEnabled=" + + isDataPrivacyEnabled + ", isDCSContractManagementSupported=" + isDCSContractManagementSupported + + ", isEasyChargeEnabled=" + isEasyChargeEnabled + ", isEvGoChargingSupported=" + + isEvGoChargingSupported + ", isMiniChargingSupported=" + isMiniChargingSupported + + ", isNonLscFeatureEnabled=" + isNonLscFeatureEnabled + ", isRemoteEngineStartSupported=" + + isRemoteEngineStartSupported + ", isRemoteHistoryDeletionSupported=" + + isRemoteHistoryDeletionSupported + ", isRemoteHistorySupported=" + isRemoteHistorySupported + + ", isRemoteParkingSupported=" + isRemoteParkingSupported + ", isRemoteServicesActivationRequired=" + + isRemoteServicesActivationRequired + ", isRemoteServicesBookingRequired=" + + isRemoteServicesBookingRequired + ", isScanAndChargeSupported=" + isScanAndChargeSupported + + ", isSustainabilityAccumulatedViewEnabled=" + isSustainabilityAccumulatedViewEnabled + + ", isSustainabilitySupported=" + isSustainabilitySupported + ", isWifiHotspotServiceSupported=" + + isWifiHotspotServiceSupported + ", lights=" + lights + ", lock=" + lock + ", remote360=" + remote360 + + ", remoteChargingCommands=" + remoteChargingCommands + ", remoteSoftwareUpgrade=" + + remoteSoftwareUpgrade + ", sendPoi=" + sendPoi + ", speechThirdPartyAlexa=" + speechThirdPartyAlexa + + ", speechThirdPartyAlexaSDK=" + speechThirdPartyAlexaSDK + ", unlock=" + unlock + ", vehicleFinder=" + + vehicleFinder + ", digitalKey=" + digitalKey + ", a4aType=" + a4aType + ", climateFunction=" + + climateFunction + ", climateTimerTrigger=" + climateTimerTrigger + ", lastStateCallState=" + + lastStateCallState + ", vehicleStateSource=" + vehicleStateSource + "]"; + } + + /** + * returns a list of capabilities filtered by the provided suffix and the enabled requirement + * + * @param suffix + * @param enabled + * @return + */ + public String getCapabilitiesAsString(String suffix, boolean enabled) { + StringBuffer capabilitiesAsString = new StringBuffer(); + List capabilitiesAsStringList = getCapabilitiesAsStringList(suffix, enabled); + + for (String capEntry : capabilitiesAsStringList) { + // remove "is" prefix and provided suffix + String cut = capEntry.substring(2); + if (cut.endsWith(suffix)) { + if (capabilitiesAsString.length() > 0) { + capabilitiesAsString.append(Constants.SEMICOLON); + } + capabilitiesAsString.append(cut.substring(0, cut.length() - suffix.length())); + } + } + return capabilitiesAsString.toString(); + } + + private List getCapabilitiesAsStringList(String suffix, boolean compare) { + List l = new ArrayList<>(); + + Arrays.asList(VehicleCapabilities.class.getDeclaredFields()).stream() + .filter(field -> field.getName().startsWith(PREFIX_IS) && field.getName().endsWith(suffix)) + .forEach(field -> { + try { + boolean value = field.getBoolean(this); + if (compare == value) { + l.add(field.getName()); + } + } catch (IllegalArgumentException | IllegalAccessException e) { + logger.trace("field {} not usable: ", field.getName()); + } + }); + + return l; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleDoorsState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleDoorsState.java new file mode 100644 index 0000000000000..55a2febd292c2 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleDoorsState.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class VehicleDoorsState { + private String combinedSecurityState = ""; // SECURED, + private String leftFront = ""; // CLOSED + private String leftRear = ""; // CLOSED + private String rightFront = ""; // CLOSED + private String rightRear = ""; // CLOSED + private String combinedState = ""; // CLOSED + private String hood = ""; // CLOSED + private String trunk = ""; // CLOSED + + public String getCombinedSecurityState() { + return combinedSecurityState; + } + + public void setCombinedSecurityState(String combinedSecurityState) { + this.combinedSecurityState = combinedSecurityState; + } + + public String getLeftFront() { + return leftFront; + } + + public void setLeftFront(String leftFront) { + this.leftFront = leftFront; + } + + public String getLeftRear() { + return leftRear; + } + + public void setLeftRear(String leftRear) { + this.leftRear = leftRear; + } + + public String getRightFront() { + return rightFront; + } + + public void setRightFront(String rightFront) { + this.rightFront = rightFront; + } + + public String getRightRear() { + return rightRear; + } + + public void setRightRear(String rightRear) { + this.rightRear = rightRear; + } + + public String getCombinedState() { + return combinedState; + } + + public void setCombinedState(String combinedState) { + this.combinedState = combinedState; + } + + public String getHood() { + return hood; + } + + public void setHood(String hood) { + this.hood = hood; + } + + public String getTrunk() { + return trunk; + } + + public void setTrunk(String trunk) { + this.trunk = trunk; + } + + @Override + public String toString() { + return "VehicleDoorsState [combinedSecurityState=" + combinedSecurityState + ", leftFront=" + leftFront + + ", leftRear=" + leftRear + ", rightFront=" + rightFront + ", rightRear=" + rightRear + + ", combinedState=" + combinedState + ", hood=" + hood + ", trunk=" + trunk + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java new file mode 100644 index 0000000000000..396bcce6d2725 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * The {@link VehicleLocation} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored to Java Bean + */ +public class VehicleLocation { + private Coordinates coordinates = new Coordinates(); + private Address address = new Address(); + private int heading = -1; + + public Coordinates getCoordinates() { + return coordinates; + } + + public void setCoordinates(Coordinates coordinates) { + this.coordinates = coordinates; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public int getHeading() { + return heading; + } + + public void setHeading(int heading) { + this.heading = heading; + } + + @Override + public String toString() { + return "Location [coordinates=" + coordinates + ", address=" + address + ", heading=" + heading + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleRoofState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleRoofState.java new file mode 100644 index 0000000000000..8b1e002c19015 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleRoofState.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class VehicleRoofState { + private String roofState = ""; // CLOSED, + private String roofStateType = ""; // SUN_ROOF + + public String getRoofState() { + return roofState; + } + + public void setRoofState(String roofState) { + this.roofState = roofState; + } + + public String getRoofStateType() { + return roofStateType; + } + + public void setRoofStateType(String roofStateType) { + this.roofStateType = roofStateType; + } + + @Override + public String toString() { + return "VehicleRoofState [roofState=" + roofState + ", roofStateType=" + roofStateType + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleState.java new file mode 100644 index 0000000000000..d9bf0a755b9b9 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleState.java @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +import java.util.ArrayList; +import java.util.List; + +import org.openhab.binding.mybmw.internal.dto.charge.ChargingProfile; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class VehicleState { + + public static final String CHECK_CONTROL_OVERALL_MESSAGE_OK = "No Issues"; + + private boolean isLeftSteering = false; + private String lastFetched = ""; // 2022-12-21T17:31:26.560Z, + private String lastUpdatedAt = ""; // 2022-12-21T15:41:23Z, + private boolean isLscSupported = false; // true, + private int range = -1; // 435, + private VehicleDoorsState doorsState = new VehicleDoorsState(); + private VehicleWindowsState windowsState = new VehicleWindowsState(); + private VehicleRoofState roofState = new VehicleRoofState(); + private VehicleTireStates tireState = new VehicleTireStates(); + + private VehicleLocation location = new VehicleLocation(); + private int currentMileage = -1; + private ClimateControlState climateControlState = new ClimateControlState(); + private List requiredServices = new ArrayList<>(); + private List checkControlMessages = new ArrayList<>(); + private CombustionFuelLevel combustionFuelLevel = new CombustionFuelLevel(); + private DriverPreferences driverPreferences = new DriverPreferences(); + private ElectricChargingState electricChargingState = new ElectricChargingState(); + private boolean isDeepSleepModeActive = false; // false + private List climateTimers = new ArrayList<>(); + private ChargingProfile chargingProfile = new ChargingProfile(); + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + /** + * @return the isLeftSteering + */ + public boolean isLeftSteering() { + return isLeftSteering; + } + + /** + * @return the lastFetched + */ + public String getLastFetched() { + return lastFetched; + } + + /** + * @return the lastUpdatedAt + */ + public String getLastUpdatedAt() { + return lastUpdatedAt; + } + + /** + * @return the isLscSupported + */ + public boolean isLscSupported() { + return isLscSupported; + } + + /** + * @return the range + */ + public int getRange() { + return range; + } + + /** + * @return the doorsState + */ + public VehicleDoorsState getDoorsState() { + return doorsState; + } + + /** + * @return the windowsState + */ + public VehicleWindowsState getWindowsState() { + return windowsState; + } + + /** + * @return the roofState + */ + public VehicleRoofState getRoofState() { + return roofState; + } + + /** + * @return the tireState + */ + public VehicleTireStates getTireState() { + return tireState; + } + + /** + * @return the location + */ + public VehicleLocation getLocation() { + return location; + } + + /** + * @return the currentMileage + */ + public int getCurrentMileage() { + return currentMileage; + } + + /** + * @return the climateControlState + */ + public ClimateControlState getClimateControlState() { + return climateControlState; + } + + /** + * @return the requiredServices + */ + public List getRequiredServices() { + return requiredServices; + } + + /** + * @return the checkControlMessages + */ + public List getCheckControlMessages() { + return checkControlMessages; + } + + /** + * @return the combustionFuelLevel + */ + public CombustionFuelLevel getCombustionFuelLevel() { + return combustionFuelLevel; + } + + /** + * @return the driverPreferences + */ + public DriverPreferences getDriverPreferences() { + return driverPreferences; + } + + /** + * @return the electricChargingState + */ + public ElectricChargingState getElectricChargingState() { + return electricChargingState; + } + + /** + * @return the isDeepSleepModeActive + */ + public boolean isDeepSleepModeActive() { + return isDeepSleepModeActive; + } + + /** + * @return the climateTimers + */ + public List getClimateTimers() { + return climateTimers; + } + + /** + * @return the chargingProfile + */ + public ChargingProfile getChargingProfile() { + return chargingProfile; + } + + @Override + public String toString() { + return "VehicleState [isLeftSteering=" + isLeftSteering + ", lastFetched=" + lastFetched + ", lastUpdatedAt=" + + lastUpdatedAt + ", isLscSupported=" + isLscSupported + ", range=" + range + ", doorsState=" + + doorsState + ", windowsState=" + windowsState + ", roofState=" + roofState + ", tireState=" + + tireState + ", location=" + location + ", currentMileage=" + currentMileage + ", climateControlState=" + + climateControlState + ", requiredServices=" + requiredServices + ", checkControlMessages=" + + checkControlMessages + ", combustionFuelLevel=" + combustionFuelLevel + ", driverPreferences=" + + driverPreferences + ", electricChargingState=" + electricChargingState + ", isDeepSleepModeActive=" + + isDeepSleepModeActive + ", climateTimers=" + climateTimers + ", chargingProfile=" + chargingProfile + + "]"; + } + + /** + * helper methods + */ + public String getOverallCheckControlStatus() { + StringBuilder overallMessage = new StringBuilder(); + + for (CheckControlMessage checkControlMessage : checkControlMessages) { + if (checkControlMessage.getId() > 0) { + overallMessage.append(checkControlMessage.getName() + "; "); + } + } + + String overallMessageString = overallMessage.toString(); + + if (overallMessageString.isEmpty()) { + overallMessageString = CHECK_CONTROL_OVERALL_MESSAGE_OK; + } + + return overallMessageString; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainer.java new file mode 100644 index 0000000000000..d854998a0ece0 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainer.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class VehicleStateContainer { + private VehicleState state = new VehicleState(); + private VehicleCapabilities capabilities = new VehicleCapabilities(); + + private String rawStateJson = ""; + + public VehicleState getState() { + return state; + } + + public void setState(VehicleState state) { + this.state = state; + } + + public VehicleCapabilities getCapabilities() { + return capabilities; + } + + public void setCapabilities(VehicleCapabilities capabilities) { + this.capabilities = capabilities; + } + + @Override + public String toString() { + return "VehicleState [state=" + state + ", capabilities=" + capabilities + "]"; + } + + public String getRawStateJson() { + return rawStateJson; + } + + public void setRawStateJson(String rawStateJson) { + this.rawStateJson = rawStateJson; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireState.java new file mode 100644 index 0000000000000..cec5b2467e328 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireState.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class VehicleTireState { + private VehicleTireStateDetails details = new VehicleTireStateDetails(); + private VehicleTireStateStatus status = new VehicleTireStateStatus(); + + public VehicleTireStateDetails getDetails() { + return details; + } + + public void setDetails(VehicleTireStateDetails details) { + this.details = details; + } + + public VehicleTireStateStatus getStatus() { + return status; + } + + public void setStatus(VehicleTireStateStatus status) { + this.status = status; + } + + @Override + public String toString() { + return "VehicleTireState [details=" + details + ", status=" + status + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetails.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetails.java new file mode 100644 index 0000000000000..a325edaa8b579 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetails.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class VehicleTireStateDetails { + private String dimension = ""; // 225/45 R18 95V XL, + private String treadDesign = ""; // Winter Contact TS 860 S SSR, + private String manufacturer = ""; // Continental, + private int manufacturingWeek = -1; // 5299, + private boolean isOptimizedForOemBmw = false; // true, + private String partNumber = ""; // 2471558, + private VehicleTireStateDetailsClassification speedClassification; // + private String mountingDate = ""; // 2022-10-06T00:00:00.000Z, + private int season = -1; // 4, + private boolean identificationInProgress = false; // false + + public String getDimension() { + return dimension; + } + + public void setDimension(String dimension) { + this.dimension = dimension; + } + + public String getTreadDesign() { + return treadDesign; + } + + public void setTreadDesign(String treadDesign) { + this.treadDesign = treadDesign; + } + + public String getManufacturer() { + return manufacturer; + } + + public void setManufacturer(String manufacturer) { + this.manufacturer = manufacturer; + } + + public int getManufacturingWeek() { + return manufacturingWeek; + } + + public void setManufacturingWeek(int manufacturingWeek) { + this.manufacturingWeek = manufacturingWeek; + } + + public boolean isOptimizedForOemBmw() { + return isOptimizedForOemBmw; + } + + public void setOptimizedForOemBmw(boolean isOptimizedForOemBmw) { + this.isOptimizedForOemBmw = isOptimizedForOemBmw; + } + + public String getPartNumber() { + return partNumber; + } + + public void setPartNumber(String partNumber) { + this.partNumber = partNumber; + } + + public VehicleTireStateDetailsClassification getSpeedClassification() { + return speedClassification; + } + + public void setSpeedClassification(VehicleTireStateDetailsClassification speedClassification) { + this.speedClassification = speedClassification; + } + + public String getMountingDate() { + return mountingDate; + } + + public void setMountingDate(String mountingDate) { + this.mountingDate = mountingDate; + } + + public int getSeason() { + return season; + } + + public void setSeason(int season) { + this.season = season; + } + + public boolean isIdentificationInProgress() { + return identificationInProgress; + } + + public void setIdentificationInProgress(boolean identificationInProgress) { + this.identificationInProgress = identificationInProgress; + } + + @Override + public String toString() { + return "VehicleTireStateDetails [dimension=" + dimension + ", treadDesign=" + treadDesign + ", manufacturer=" + + manufacturer + ", manufacturingWeek=" + manufacturingWeek + ", isOptimizedForOemBmw=" + + isOptimizedForOemBmw + ", partNumber=" + partNumber + ", speedClassification=" + speedClassification + + ", mountingDate=" + mountingDate + ", season=" + season + ", identificationInProgress=" + + identificationInProgress + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetailsClassification.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetailsClassification.java new file mode 100644 index 0000000000000..03ad0646a1eac --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetailsClassification.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class VehicleTireStateDetailsClassification { + private int speedRating = -1; // 240, + private boolean atLeast = false; // false + + public int getSpeedRating() { + return speedRating; + } + + public void setSpeedRating(int speedRating) { + this.speedRating = speedRating; + } + + public boolean isAtLeast() { + return atLeast; + } + + public void setAtLeast(boolean atLeast) { + this.atLeast = atLeast; + } + + @Override + public String toString() { + return "VehicleTireStateDetailsClassification [speedRating=" + speedRating + ", atLeast=" + atLeast + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateStatus.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateStatus.java new file mode 100644 index 0000000000000..92471802a1ab5 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateStatus.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from API response + * + * @author Martin Grassl - initial contribution + */ +public class VehicleTireStateStatus { + private int currentPressure = -1; // 280, + private int targetPressure = -1; // 290 + + public int getCurrentPressure() { + return currentPressure; + } + + public void setCurrentPressure(int currentPressure) { + this.currentPressure = currentPressure; + } + + public int getTargetPressure() { + return targetPressure; + } + + public void setTargetPressure(int targetPressure) { + this.targetPressure = targetPressure; + } + + @Override + public String toString() { + return "VehicleTireStateStatus [currentPressure=" + currentPressure + ", targetPressure=" + targetPressure + + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStates.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStates.java new file mode 100644 index 0000000000000..17ca131617061 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStates.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class VehicleTireStates { + private VehicleTireState frontLeft = new VehicleTireState(); + private VehicleTireState frontRight = new VehicleTireState(); + private VehicleTireState rearLeft = new VehicleTireState(); + private VehicleTireState rearRight = new VehicleTireState(); + + public VehicleTireState getFrontLeft() { + return frontLeft; + } + + public void setFrontLeft(VehicleTireState frontLeft) { + this.frontLeft = frontLeft; + } + + public VehicleTireState getFrontRight() { + return frontRight; + } + + public void setFrontRight(VehicleTireState frontRight) { + this.frontRight = frontRight; + } + + public VehicleTireState getRearLeft() { + return rearLeft; + } + + public void setRearLeft(VehicleTireState rearLeft) { + this.rearLeft = rearLeft; + } + + public VehicleTireState getRearRight() { + return rearRight; + } + + public void setRearRight(VehicleTireState rearRight) { + this.rearRight = rearRight; + } + + @Override + public String toString() { + return "VehicleTireStates [frontLeft=" + frontLeft + ", frontRight=" + frontRight + ", rearLeft=" + rearLeft + + ", rearRight=" + rearRight + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleWindowsState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleWindowsState.java new file mode 100644 index 0000000000000..50fc815d0d00e --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleWindowsState.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +/** + * + * derived from the API responses + * + * @author Martin Grassl - initial contribution + */ +public class VehicleWindowsState { + private String leftFront = ""; // CLOSED, + private String leftRear = ""; // CLOSED, + private String rightFront = ""; // CLOSED, + private String rightRear = ""; // CLOSED, + private String rear = ""; // CLOSED, + private String combinedState = ""; // CLOSED + + public String getLeftFront() { + return leftFront; + } + + public void setLeftFront(String leftFront) { + this.leftFront = leftFront; + } + + public String getLeftRear() { + return leftRear; + } + + public void setLeftRear(String leftRear) { + this.leftRear = leftRear; + } + + public String getRightFront() { + return rightFront; + } + + public void setRightFront(String rightFront) { + this.rightFront = rightFront; + } + + public String getRightRear() { + return rightRear; + } + + public void setRightRear(String rightRear) { + this.rightRear = rightRear; + } + + public String getRear() { + return rear; + } + + public void setRear(String rear) { + this.rear = rear; + } + + public String getCombinedState() { + return combinedState; + } + + public void setCombinedState(String combinedState) { + this.combinedState = combinedState; + } + + @Override + public String toString() { + return "VehicleWindowsState [leftFront=" + leftFront + ", leftRear=" + leftRear + ", rightFront=" + rightFront + + ", rightRear=" + rightRear + ", rear=" + rear + ", combinedState=" + combinedState + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java index 91c3b5b06af87..bf5ec4bb80651 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java @@ -14,20 +14,18 @@ import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mybmw.internal.MyBMWConfiguration; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; import org.openhab.binding.mybmw.internal.discovery.VehicleDiscovery; -import org.openhab.binding.mybmw.internal.dto.network.NetworkError; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; -import org.openhab.binding.mybmw.internal.utils.BimmerConstants; +import org.openhab.binding.mybmw.internal.handler.backend.MyBMWFileProxy; +import org.openhab.binding.mybmw.internal.handler.backend.MyBMWHttpProxy; +import org.openhab.binding.mybmw.internal.handler.backend.MyBMWProxy; import org.openhab.binding.mybmw.internal.utils.Constants; -import org.openhab.binding.mybmw.internal.utils.Converter; +import org.openhab.binding.mybmw.internal.utils.MyBMWConfigurationChecker; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; @@ -40,19 +38,26 @@ import org.slf4j.LoggerFactory; /** - * The {@link MyBMWBridgeHandler} is responsible for handling commands, which are + * The {@link MyBMWBridgeHandler} is responsible for handling commands, which + * are * sent to one of the channels. * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactored, all discovery functionality moved to VehicleDiscovery */ @NonNullByDefault -public class MyBMWBridgeHandler extends BaseBridgeHandler implements StringResponseCallback { +public class MyBMWBridgeHandler extends BaseBridgeHandler { + + private static final String ENVIRONMENT = "ENVIRONMENT"; + private static final String TEST = "test"; + private static final String TESTUSER = "testuser"; + private final Logger logger = LoggerFactory.getLogger(MyBMWBridgeHandler.class); + private HttpClientFactory httpClientFactory; - private Optional discoveryService = Optional.empty(); - private Optional proxy = Optional.empty(); + private Optional myBmwProxy = Optional.empty(); private Optional> initializerJob = Optional.empty(); - private Optional troubleshootFingerprint = Optional.empty(); + private Optional vehicleDiscovery = Optional.empty(); private String localeLanguage; public MyBMWBridgeHandler(Bridge bridge, HttpClientFactory hcf, String language) { @@ -61,81 +66,78 @@ public MyBMWBridgeHandler(Bridge bridge, HttpClientFactory hcf, String language) localeLanguage = language; } + public void setVehicleDiscovery(VehicleDiscovery vehicleDiscovery) { + logger.trace("xxxMyBMWBridgeHandler.setVehicleDiscovery"); + this.vehicleDiscovery = Optional.of(vehicleDiscovery); + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { // no commands available + logger.trace("xxxMyBMWBridgeHandler.handleCommand"); } @Override public void initialize() { - troubleshootFingerprint = Optional.empty(); + logger.trace("xxxMyBMWBridgeHandler.initialize"); updateStatus(ThingStatus.UNKNOWN); - MyBMWConfiguration config = getConfigAs(MyBMWConfiguration.class); + MyBMWBridgeConfiguration config = getConfigAs(MyBMWBridgeConfiguration.class); if (config.language.equals(Constants.LANGUAGE_AUTODETECT)) { config.language = localeLanguage; } - if (!checkConfiguration(config)) { + if (!MyBMWConfigurationChecker.checkConfiguration(config)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); } else { - proxy = Optional.of(new MyBMWProxy(httpClientFactory, config)); - initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 2, TimeUnit.SECONDS)); - } - } + // there is no risk in this functionality as several steps have to happen to get the file proxy working: + // 1. environment variable ENVIRONMENT has to be available + // 2. username of the myBMW account must be set to "testuser" which is anyhow no valid username + // 3. the jar file must contain the fingerprints which will only happen if it has been built with the + // test-jar profile + String environment = System.getenv(ENVIRONMENT); - public static boolean checkConfiguration(MyBMWConfiguration config) { - if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) { - return false; - } else { - return BimmerConstants.EADRAX_SERVER_MAP.containsKey(config.region); + if (!(TEST.equals(environment) && TESTUSER.equals(config.userName))) { + myBmwProxy = Optional.of(new MyBMWHttpProxy(httpClientFactory, config)); + } else { + myBmwProxy = Optional.of(new MyBMWFileProxy(httpClientFactory, config)); + } + initializerJob = Optional.of(scheduler.schedule(this::discoverVehicles, 2, TimeUnit.SECONDS)); } } @Override public void dispose() { + logger.trace("xxxMyBMWBridgeHandler.dispose"); initializerJob.ifPresent(job -> job.cancel(true)); } - public void requestVehicles() { - proxy.ifPresent(prox -> prox.requestVehicles(this)); + public void vehicleDiscoveryError() { + logger.trace("xxxMyBMWBridgeHandler.vehicleDiscoveryError"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Request vehicles failed"); } - private void logFingerPrint() { - logger.debug("###### Discovery Fingerprint Data - BEGIN ######"); - logger.debug("{}", troubleshootFingerprint.get()); - logger.debug("###### Discovery Fingerprint Data - END ######"); + public void vehicleDiscoverySuccess() { + logger.trace("xxxMyBMWBridgeHandler.vehicleDiscoverySuccess"); + updateStatus(ThingStatus.ONLINE); } - /** - * Response for vehicle request - */ - @Override - public synchronized void onResponse(@Nullable String response) { - if (response != null) { - updateStatus(ThingStatus.ONLINE); - List vehicleList = Converter.getVehicleList(response); - discoveryService.get().onResponse(vehicleList); - troubleshootFingerprint = Optional.of(Converter.anonymousFingerprint(response)); - logFingerPrint(); - } - } + private void discoverVehicles() { + logger.trace("xxxMyBMWBridgeHandler.requestVehicles"); - @Override - public void onError(NetworkError error) { - troubleshootFingerprint = Optional.of(error.toJson()); - logFingerPrint(); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason); + MyBMWBridgeConfiguration config = getConfigAs(MyBMWBridgeConfiguration.class); + + myBmwProxy.ifPresent(proxy -> proxy.setBridgeConfiguration(config)); + + vehicleDiscovery.ifPresent(discovery -> discovery.discoverVehicles()); } @Override public Collection> getServices() { + logger.trace("xxxMyBMWBridgeHandler.getServices"); return Collections.singleton(VehicleDiscovery.class); } - public Optional getProxy() { - return proxy; - } - - public void setDiscoveryService(VehicleDiscovery discoveryService) { - this.discoveryService = Optional.of(discoveryService); + public Optional getMyBmwProxy() { + logger.trace("xxxMyBMWBridgeHandler.getProxy"); + return myBmwProxy; } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java deleted file mode 100644 index e29021e0e150c..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWProxy.java +++ /dev/null @@ -1,515 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler; - -import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.*; - -import java.nio.charset.StandardCharsets; -import java.security.KeyFactory; -import java.security.MessageDigest; -import java.security.PublicKey; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -import javax.crypto.Cipher; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.HttpResponseException; -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Result; -import org.eclipse.jetty.client.util.BufferingResponseListener; -import org.eclipse.jetty.client.util.StringContentProvider; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.util.MultiMap; -import org.eclipse.jetty.util.UrlEncoded; -import org.openhab.binding.mybmw.internal.MyBMWConfiguration; -import org.openhab.binding.mybmw.internal.VehicleConfiguration; -import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse; -import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse; -import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse; -import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration; -import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse; -import org.openhab.binding.mybmw.internal.dto.network.NetworkError; -import org.openhab.binding.mybmw.internal.handler.simulation.Injector; -import org.openhab.binding.mybmw.internal.utils.BimmerConstants; -import org.openhab.binding.mybmw.internal.utils.Constants; -import org.openhab.binding.mybmw.internal.utils.Converter; -import org.openhab.binding.mybmw.internal.utils.HTTPConstants; -import org.openhab.binding.mybmw.internal.utils.ImageProperties; -import org.openhab.core.io.net.http.HttpClientFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link MyBMWProxy} This class holds the important constants for the BMW Connected Drive Authorization. - * They - * are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected} - * File defining these constants - * {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py} - * https://customer.bmwgroup.com/one/app/oauth.js - * - * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - edit & send of charge profile - */ -@NonNullByDefault -public class MyBMWProxy { - private final Logger logger = LoggerFactory.getLogger(MyBMWProxy.class); - private Optional remoteServiceHandler = Optional.empty(); - private final Token token = new Token(); - private final HttpClient httpClient; - private final MyBMWConfiguration configuration; - - /** - * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py - */ - final String vehicleUrl; - final String remoteCommandUrl; - final String remoteStatusUrl; - final String serviceExecutionAPI = "/executeService"; - final String serviceExecutionStateAPI = "/serviceExecutionStatus"; - final String remoteServiceEADRXstatusUrl = BimmerConstants.API_REMOTE_SERVICE_BASE_URL - + "eventStatus?eventId={event_id}"; - - public MyBMWProxy(HttpClientFactory httpClientFactory, MyBMWConfiguration config) { - httpClient = httpClientFactory.getCommonHttpClient(); - configuration = config; - - vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region) - + BimmerConstants.API_VEHICLES; - - remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region) - + BimmerConstants.API_REMOTE_SERVICE_BASE_URL; - remoteStatusUrl = remoteCommandUrl + "eventStatus"; - } - - public synchronized void call(final String url, final boolean post, final @Nullable String encoding, - final @Nullable String params, final String brand, final ResponseCallback callback) { - // only executed in "simulation mode" - // SimulationTest.testSimulationOff() assures Injector is off when releasing - if (Injector.isActive()) { - if (url.equals(vehicleUrl)) { - ((StringResponseCallback) callback).onResponse(Injector.getDiscovery()); - } else if (url.endsWith(vehicleUrl)) { - ((StringResponseCallback) callback).onResponse(Injector.getStatus()); - } else { - logger.debug("Simulation of {} not supported", url); - } - return; - } - - // return in case of unknown brand - if (!BimmerConstants.ALL_BRANDS.contains(brand.toLowerCase())) { - logger.warn("Unknown Brand {}", brand); - return; - } - - final Request req; - final String completeUrl; - - if (post) { - completeUrl = url; - req = httpClient.POST(url); - if (encoding != null) { - req.header(HttpHeader.CONTENT_TYPE, encoding); - if (CONTENT_TYPE_URL_ENCODED.equals(encoding)) { - req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8)); - } else if (CONTENT_TYPE_JSON_ENCODED.equals(encoding)) { - req.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8)); - } - } - } else { - completeUrl = params == null ? url : url + Constants.QUESTION + params; - req = httpClient.newRequest(completeUrl); - } - req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken()); - req.header(HTTPConstants.X_USER_AGENT, - String.format(BimmerConstants.X_USER_AGENT, brand, configuration.region)); - req.header(HttpHeader.ACCEPT_LANGUAGE, configuration.language); - if (callback instanceof ByteResponseCallback) { - req.header(HttpHeader.ACCEPT, "image/png"); - } else { - req.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON_ENCODED); - } - - req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() { - @NonNullByDefault({}) - @Override - public void onComplete(Result result) { - if (result.getResponse().getStatus() != 200) { - NetworkError error = new NetworkError(); - error.url = completeUrl; - error.status = result.getResponse().getStatus(); - if (result.getResponse().getReason() != null) { - error.reason = result.getResponse().getReason(); - } else { - error.reason = result.getFailure().getMessage(); - } - error.params = result.getRequest().getParams().toString(); - logger.debug("HTTP Error {}", error.toString()); - callback.onError(error); - } else { - if (callback instanceof StringResponseCallback) { - ((StringResponseCallback) callback).onResponse(getContentAsString()); - } else if (callback instanceof ByteResponseCallback) { - ((ByteResponseCallback) callback).onResponse(getContent()); - } else { - logger.error("unexpected reponse type {}", callback.getClass().getName()); - } - } - } - }); - } - - public void get(String url, @Nullable String coding, @Nullable String params, final String brand, - ResponseCallback callback) { - call(url, false, coding, params, brand, callback); - } - - public void post(String url, @Nullable String coding, @Nullable String params, final String brand, - ResponseCallback callback) { - call(url, true, coding, params, brand, callback); - } - - /** - * request all vehicles for one specific brand - * - * @param brand - * @param callback - */ - public void requestVehicles(String brand, StringResponseCallback callback) { - // calculate necessary parameters for query - MultiMap vehicleParams = new MultiMap(); - vehicleParams.put(BimmerConstants.TIRE_GUARD_MODE, Constants.ENABLED); - vehicleParams.put(BimmerConstants.APP_DATE_TIME, Long.toString(System.currentTimeMillis())); - vehicleParams.put(BimmerConstants.APP_TIMEZONE, Integer.toString(Converter.getOffsetMinutes())); - String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false); - get(vehicleUrl + "?" + params, null, null, brand, callback); - } - - /** - * request vehicles for all possible brands - * - * @param callback - */ - public void requestVehicles(StringResponseCallback callback) { - BimmerConstants.ALL_BRANDS.forEach(brand -> { - requestVehicles(brand, callback); - }); - } - - public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) { - final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region) - + "/eadrax-ics/v3/presentation/vehicles/" + config.vin + "/images?carView=" + props.viewport; - get(localImageUrl, null, null, config.vehicleBrand, callback); - } - - /** - * request charge statistics for electric vehicles - * - * @param callback - */ - public void requestChargeStatistics(VehicleConfiguration config, StringResponseCallback callback) { - MultiMap chargeStatisticsParams = new MultiMap(); - chargeStatisticsParams.put("vin", config.vin); - chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime()); - String params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false); - String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region) - + "/eadrax-chs/v1/charging-statistics?" + params; - get(chargeStatisticsUrl, null, null, config.vehicleBrand, callback); - } - - /** - * request charge statistics for electric vehicles - * - * @param callback - */ - public void requestChargeSessions(VehicleConfiguration config, StringResponseCallback callback) { - MultiMap chargeSessionsParams = new MultiMap(); - chargeSessionsParams.put("vin", "WBY1Z81040V905639"); - chargeSessionsParams.put("maxResults", "40"); - chargeSessionsParams.put("include_date_picker", "true"); - String params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false); - String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region) - + "/eadrax-chs/v1/charging-sessions?" + params; - - get(chargeSessionsUrl, null, null, config.vehicleBrand, callback); - } - - RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) { - remoteServiceHandler = Optional.of(new RemoteServiceHandler(vehicleHandler, this)); - return remoteServiceHandler.get(); - } - - // Token handling - - /** - * Gets new token if old one is expired or invalid. In case of error the token remains. - * So if token refresh fails the corresponding requests will also fail and update the - * Thing status accordingly. - * - * @return token - */ - public Token getToken() { - if (!token.isValid()) { - boolean tokenUpdateSuccess = false; - switch (configuration.region) { - case BimmerConstants.REGION_CHINA: - tokenUpdateSuccess = updateTokenChina(); - break; - case BimmerConstants.REGION_NORTH_AMERICA: - tokenUpdateSuccess = updateToken(); - break; - case BimmerConstants.REGION_ROW: - tokenUpdateSuccess = updateToken(); - break; - default: - logger.warn("Region {} not supported", configuration.region); - break; - } - if (!tokenUpdateSuccess) { - logger.debug("Authorization failed!"); - } - } - return token; - } - - /** - * Everything is catched by surroundig try catch - * - HTTP Exceptions - * - JSONSyntax Exceptions - * - potential NullPointer Exceptions - * - * @return - */ - @SuppressWarnings("null") - public synchronized boolean updateToken() { - try { - /* - * Step 1) Get basic values for further queries - */ - String authValuesUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region) - + BimmerConstants.API_OAUTH_CONFIG; - Request authValuesRequest = httpClient.newRequest(authValuesUrl); - authValuesRequest.header(ACP_SUBSCRIPTION_KEY, BimmerConstants.OCP_APIM_KEYS.get(configuration.region)); - authValuesRequest.header(X_USER_AGENT, - String.format(BimmerConstants.X_USER_AGENT, BimmerConstants.BRAND_BMW, configuration.region)); - - ContentResponse authValuesResponse = authValuesRequest.send(); - if (authValuesResponse.getStatus() != 200) { - throw new HttpResponseException("URL: " + authValuesRequest.getURI() + ", Error: " - + authValuesResponse.getStatus() + ", Message: " + authValuesResponse.getContentAsString(), - authValuesResponse); - } - AuthQueryResponse aqr = Converter.getGson().fromJson(authValuesResponse.getContentAsString(), - AuthQueryResponse.class); - - /* - * Step 2) Calculate values for base parameters - */ - String verfifierBytes = Converter.getRandomString(64); - String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verfifierBytes.getBytes()); - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8)); - String codeChallange = Base64.getUrlEncoder().withoutPadding().encodeToString(hash); - String stateBytes = Converter.getRandomString(16); - String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes()); - - MultiMap baseParams = new MultiMap(); - baseParams.put(CLIENT_ID, aqr.clientId); - baseParams.put(RESPONSE_TYPE, CODE); - baseParams.put(REDIRECT_URI, aqr.returnUrl); - baseParams.put(STATE, state); - baseParams.put(NONCE, BimmerConstants.LOGIN_NONCE); - baseParams.put(SCOPE, String.join(Constants.SPACE, aqr.scopes)); - baseParams.put(CODE_CHALLENGE, codeChallange); - baseParams.put(CODE_CHALLENGE_METHOD, "S256"); - - /** - * Step 3) Authorization with username and password - */ - String loginUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT; - Request loginRequest = httpClient.POST(loginUrl); - loginRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); - - MultiMap loginParams = new MultiMap(baseParams); - loginParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE); - loginParams.put(USERNAME, configuration.userName); - loginParams.put(PASSWORD, configuration.password); - loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, - UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); - ContentResponse loginResponse = loginRequest.send(); - if (loginResponse.getStatus() != 200) { - throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: " - + loginResponse.getStatus() + ", Message: " + loginResponse.getContentAsString(), - loginResponse); - } - String authCode = getAuthCode(loginResponse.getContentAsString()); - - /** - * Step 4) Authorize with code - */ - Request authRequest = httpClient.POST(loginUrl).followRedirects(false); - MultiMap authParams = new MultiMap(baseParams); - authParams.put(AUTHORIZATION, authCode); - authRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); - authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, - UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); - ContentResponse authResponse = authRequest.send(); - if (authResponse.getStatus() != 302) { - throw new HttpResponseException("URL: " + authRequest.getURI() + ", Error: " + authResponse.getStatus() - + ", Message: " + authResponse.getContentAsString(), authResponse); - } - String code = MyBMWProxy.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION)); - - /** - * Step 5) Request token - */ - Request codeRequest = httpClient.POST(aqr.tokenEndpoint); - String basicAuth = "Basic " - + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes()); - codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); - codeRequest.header(AUTHORIZATION, basicAuth); - - MultiMap codeParams = new MultiMap(); - codeParams.put(CODE, code); - codeParams.put(CODE_VERIFIER, codeVerifier); - codeParams.put(REDIRECT_URI, aqr.returnUrl); - codeParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE); - codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, - UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); - ContentResponse codeResponse = codeRequest.send(); - if (codeResponse.getStatus() != 200) { - throw new HttpResponseException("URL: " + codeRequest.getURI() + ", Error: " + codeResponse.getStatus() - + ", Message: " + codeResponse.getContentAsString(), codeResponse); - } - AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class); - token.setType(ar.tokenType); - token.setToken(ar.accessToken); - token.setExpiration(ar.expiresIn); - return true; - } catch (Exception e) { - logger.warn("Authorization Exception: {}", e.getMessage()); - } - return false; - } - - private String getAuthCode(String response) { - String[] keys = response.split("&"); - for (int i = 0; i < keys.length; i++) { - if (keys[i].startsWith(AUTHORIZATION)) { - String authCode = keys[i].split("=")[1]; - authCode = authCode.split("\"")[0]; - return authCode; - } - } - return Constants.EMPTY; - } - - public static String codeFromUrl(String encodedUrl) { - final MultiMap tokenMap = new MultiMap(); - UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII); - final StringBuilder codeFound = new StringBuilder(); - tokenMap.forEach((key, value) -> { - if (value.size() > 0) { - String val = value.get(0); - if (key.endsWith(CODE)) { - codeFound.append(val.toString()); - } - } - }); - return codeFound.toString(); - } - - @SuppressWarnings("null") - public synchronized boolean updateTokenChina() { - try { - /** - * Step 1) get public key - */ - String publicKeyUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA) - + BimmerConstants.CHINA_PUBLIC_KEY; - Request oauthQueryRequest = httpClient.newRequest(publicKeyUrl); - oauthQueryRequest.header(HttpHeader.USER_AGENT, BimmerConstants.USER_AGENT); - oauthQueryRequest.header(X_USER_AGENT, - String.format(BimmerConstants.X_USER_AGENT, BimmerConstants.BRAND_BMW, configuration.region)); - ContentResponse publicKeyResponse = oauthQueryRequest.send(); - if (publicKeyResponse.getStatus() != 200) { - throw new HttpResponseException("URL: " + oauthQueryRequest.getURI() + ", Error: " - + publicKeyResponse.getStatus() + ", Message: " + publicKeyResponse.getContentAsString(), - publicKeyResponse); - } - ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponse.getContentAsString(), - ChinaPublicKeyResponse.class); - - /** - * Step 2) Encode password with public key - */ - // https://www.baeldung.com/java-read-pem-file-keys - String publicKeyStr = pkr.data.value; - String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "") - .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "") - .replace("\\n", "").trim(); - byte[] encoded = Base64.getDecoder().decode(publicKeyPEM); - X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded); - KeyFactory kf = KeyFactory.getInstance("RSA"); - PublicKey publicKey = kf.generatePublic(spec); - // https://www.thexcoders.net/java-ciphers-rsa/ - Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - cipher.init(Cipher.ENCRYPT_MODE, publicKey); - byte[] encryptedBytes = cipher.doFinal(configuration.password.getBytes()); - String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes); - - /** - * Step 3) Send Auth with encoded password - */ - String tokenUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA) - + BimmerConstants.CHINA_LOGIN; - Request loginRequest = httpClient.POST(tokenUrl); - loginRequest.header(X_USER_AGENT, - String.format(BimmerConstants.X_USER_AGENT, BimmerConstants.BRAND_BMW, configuration.region)); - String jsonContent = "{ \"mobile\":\"" + configuration.userName + "\", \"password\":\"" + encodedPassword - + "\"}"; - loginRequest.content(new StringContentProvider(jsonContent)); - ContentResponse tokenResponse = loginRequest.send(); - if (tokenResponse.getStatus() != 200) { - throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: " - + tokenResponse.getStatus() + ", Message: " + tokenResponse.getContentAsString(), - tokenResponse); - } - String authCode = getAuthCode(tokenResponse.getContentAsString()); - - /** - * Step 4) Decode access token - */ - ChinaTokenResponse cat = Converter.getGson().fromJson(authCode, ChinaTokenResponse.class); - String token = cat.data.accessToken; - // https://www.baeldung.com/java-jwt-token-decode - String[] chunks = token.split("\\."); - String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1])); - ChinaTokenExpiration cte = Converter.getGson().fromJson(tokenJwtDecodeStr, ChinaTokenExpiration.class); - Token t = new Token(); - t.setToken(token); - t.setType(cat.data.tokenType); - t.setExpirationTotal(cte.exp); - return true; - } catch (Exception e) { - logger.warn("Authorization Exception: {}", e.getMessage()); - } - return false; - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java new file mode 100644 index 0000000000000..fc7e6daccbbff --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler; + +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; +import org.openhab.binding.mybmw.internal.handler.backend.MyBMWProxy; +import org.openhab.binding.mybmw.internal.handler.backend.NetworkException; +import org.openhab.binding.mybmw.internal.handler.enums.ExecutionState; +import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; +import org.openhab.binding.mybmw.internal.utils.Constants; +import org.openhab.binding.mybmw.internal.utils.HTTPConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RemoteServiceExecutor} 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 + * @author Martin Grassl - rename and refactor for v2 + */ +@NonNullByDefault +public class RemoteServiceExecutor { + private final Logger logger = LoggerFactory.getLogger(RemoteServiceExecutor.class); + + private static final int GIVEUP_COUNTER = 12; // after 12 retries the state update will give up + private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec + + private final MyBMWProxy proxy; + private final VehicleHandler handler; + + private int counter = 0; + private Optional> stateJob = Optional.empty(); + private Optional serviceExecuting = Optional.empty(); + private Optional executingEventId = Optional.empty(); + + public RemoteServiceExecutor(VehicleHandler vehicleHandler, MyBMWProxy myBmwProxy) { + handler = vehicleHandler; + proxy = myBmwProxy; + } + + public boolean execute(RemoteService service) { + synchronized (this) { + if (serviceExecuting.isPresent()) { + logger.debug("Execution rejected - {} still pending", serviceExecuting.get()); + // only one service executing + return false; + } + serviceExecuting = Optional.of(service.getId()); + } + try { + ExecutionStatusContainer executionStatus = proxy.executeRemoteServiceCall( + handler.getVehicleConfiguration().get().getVin(), + handler.getVehicleConfiguration().get().getVehicleBrand(), service); + handleRemoteExecution(executionStatus); + } catch (NetworkException e) { + handleRemoteServiceException(e); + } + + return true; + } + + private void getState() { + synchronized (this) { + serviceExecuting.ifPresentOrElse(service -> { + if (counter >= GIVEUP_COUNTER) { + logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER); + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), + ExecutionState.TIMEOUT.name().toLowerCase()); + reset(); + // immediately refresh data + handler.getData(); + } else { + counter++; + try { + ExecutionStatusContainer executionStatusContainer = proxy.executeRemoteServiceStatusCall( + handler.getVehicleConfiguration().get().getVehicleBrand(), executingEventId.get()); + handleRemoteExecution(executionStatusContainer); + } catch (NetworkException e) { + handleRemoteServiceException(e); + } + } + }, () -> { + logger.warn("No Service executed to get state"); + }); + stateJob = Optional.empty(); + } + } + + private void handleRemoteServiceException(NetworkException e) { + synchronized (this) { + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), + ExecutionState.ERROR.name().toLowerCase() + Constants.SPACE + Integer.toString(e.getStatus())); + reset(); + } + } + + private void handleRemoteExecution(ExecutionStatusContainer executionStatusContainer) { + if (!executionStatusContainer.getEventId().isEmpty()) { + // service initiated - store event id for further MyBMW updates + executingEventId = Optional.of(executionStatusContainer.getEventId()); + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), + ExecutionState.INITIATED.name().toLowerCase()); + } else if (!executionStatusContainer.getEventStatus().isEmpty()) { + // service status updated + synchronized (this) { + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), + executionStatusContainer.getEventStatus().toLowerCase()); + if (ExecutionState.EXECUTED.name().equalsIgnoreCase(executionStatusContainer.getEventStatus()) + || ExecutionState.ERROR.name().equalsIgnoreCase(executionStatusContainer.getEventStatus())) { + // refresh loop ends - update of status handled in the normal refreshInterval. + // Earlier update doesn't show better results! + reset(); + return; + } + } + } + + // 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)); + } + } + + private void reset() { + serviceExecuting = Optional.empty(); + executingEventId = Optional.empty(); + counter = 0; + } + + public void cancel() { + synchronized (this) { + stateJob.ifPresent(action -> { + if (!action.isDone()) { + action.cancel(true); + } + stateJob = Optional.empty(); + }); + } + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java deleted file mode 100644 index 3fd881c103611..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceHandler.java +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler; - -import static org.openhab.binding.mybmw.internal.MyBMWConstants.*; -import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_JSON_ENCODED; - -import java.nio.charset.StandardCharsets; -import java.util.Optional; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.util.MultiMap; -import org.eclipse.jetty.util.UrlEncoded; -import org.openhab.binding.mybmw.internal.VehicleConfiguration; -import org.openhab.binding.mybmw.internal.dto.network.NetworkError; -import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; -import org.openhab.binding.mybmw.internal.utils.Constants; -import org.openhab.binding.mybmw.internal.utils.Converter; -import org.openhab.binding.mybmw.internal.utils.HTTPConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.JsonSyntaxException; - -/** - * The {@link RemoteServiceHandler} handles executions of remote services towards your Vehicle - * - * @see https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py - * - * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - edit & send of charge profile - */ -@NonNullByDefault -public class RemoteServiceHandler implements StringResponseCallback { - private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class); - - private static final String EVENT_ID = "eventId"; - private static final String DATA = "data"; - private static final int GIVEUP_COUNTER = 12; // after 12 retries the state update will give up - private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec - - private final MyBMWProxy proxy; - private final VehicleHandler handler; - private final String serviceExecutionAPI; - private final String serviceExecutionStateAPI; - - private int counter = 0; - private Optional> stateJob = Optional.empty(); - private Optional serviceExecuting = Optional.empty(); - private Optional executingEventId = Optional.empty(); - - public enum ExecutionState { - READY, - INITIATED, - PENDING, - DELIVERED, - EXECUTED, - ERROR, - TIMEOUT - } - - public enum RemoteService { - LIGHT_FLASH("Flash Lights", REMOTE_SERVICE_LIGHT_FLASH, REMOTE_SERVICE_LIGHT_FLASH), - VEHICLE_FINDER("Vehicle Finder", REMOTE_SERVICE_VEHICLE_FINDER, REMOTE_SERVICE_VEHICLE_FINDER), - DOOR_LOCK("Door Lock", REMOTE_SERVICE_DOOR_LOCK, REMOTE_SERVICE_DOOR_LOCK), - DOOR_UNLOCK("Door Unlock", REMOTE_SERVICE_DOOR_UNLOCK, REMOTE_SERVICE_DOOR_UNLOCK), - HORN_BLOW("Horn Blow", REMOTE_SERVICE_HORN, REMOTE_SERVICE_HORN), - CLIMATE_NOW_START("Start Climate", REMOTE_SERVICE_AIR_CONDITIONING_START, "climate-now?action=START"), - CLIMATE_NOW_STOP("Stop Climate", REMOTE_SERVICE_AIR_CONDITIONING_STOP, "climate-now?action=STOP"); - - private final String label; - private final String id; - private final String command; - - RemoteService(final String label, final String id, String command) { - this.label = label; - this.id = id; - this.command = command; - } - - public String getLabel() { - return label; - } - - public String getId() { - return id; - } - - public String getCommand() { - return command; - } - } - - public RemoteServiceHandler(VehicleHandler vehicleHandler, MyBMWProxy myBmwProxy) { - handler = vehicleHandler; - proxy = myBmwProxy; - final VehicleConfiguration config = handler.getConfiguration().get(); - serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/"; - serviceExecutionStateAPI = proxy.remoteStatusUrl; - } - - boolean execute(RemoteService service, String... data) { - synchronized (this) { - if (serviceExecuting.isPresent()) { - logger.debug("Execution rejected - {} still pending", serviceExecuting.get()); - // only one service executing - return false; - } - serviceExecuting = Optional.of(service.getId()); - } - final MultiMap dataMap = new MultiMap(); - if (data.length > 0) { - dataMap.add(DATA, data[0]); - proxy.post(serviceExecutionAPI + service.getCommand(), CONTENT_TYPE_JSON_ENCODED, data[0], - handler.getConfiguration().get().vehicleBrand, this); - } else { - proxy.post(serviceExecutionAPI + service.getCommand(), null, null, - handler.getConfiguration().get().vehicleBrand, this); - } - return true; - } - - public void getState() { - synchronized (this) { - serviceExecuting.ifPresentOrElse(service -> { - if (counter >= GIVEUP_COUNTER) { - logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER); - handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), - ExecutionState.TIMEOUT.name().toLowerCase()); - reset(); - // immediately refresh data - handler.getData(); - } else { - counter++; - final MultiMap dataMap = new MultiMap(); - dataMap.add(EVENT_ID, executingEventId.get()); - final String encoded = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); - proxy.post(serviceExecutionStateAPI + Constants.QUESTION + encoded, null, null, - handler.getConfiguration().get().vehicleBrand, this); - } - }, () -> { - logger.warn("No Service executed to get state"); - }); - stateJob = Optional.empty(); - } - } - - @Override - public void onResponse(@Nullable String result) { - if (result != null) { - try { - ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class); - if (esc != null) { - if (esc.eventId != null) { - // service initiated - store event id for further MyBMW updates - executingEventId = Optional.of(esc.eventId); - handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), - ExecutionState.INITIATED.name().toLowerCase()); - } else if (esc.eventStatus != null) { - // service status updated - synchronized (this) { - handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), - esc.eventStatus.toLowerCase()); - if (ExecutionState.EXECUTED.name().equalsIgnoreCase(esc.eventStatus) - || ExecutionState.ERROR.name().equalsIgnoreCase(esc.eventStatus)) { - // refresh loop ends - update of status handled in the normal refreshInterval. - // Earlier update doesn't show better results! - reset(); - return; - } - } - } - } - } catch (JsonSyntaxException jse) { - logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage()); - } - } - // schedule even if no result is present until retries exceeded - synchronized (this) { - stateJob.ifPresent(job -> { - if (!job.isDone()) { - job.cancel(true); - } - }); - stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS)); - } - } - - @Override - public void onError(NetworkError error) { - synchronized (this) { - handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), - ExecutionState.ERROR.name().toLowerCase() + Constants.SPACE + Integer.toString(error.status)); - reset(); - } - } - - private void reset() { - serviceExecuting = Optional.empty(); - executingEventId = Optional.empty(); - counter = 0; - } - - public void cancel() { - synchronized (this) { - stateJob.ifPresent(action -> { - if (!action.isDone()) { - action.cancel(true); - } - stateJob = Optional.empty(); - }); - } - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java deleted file mode 100644 index 5dcd1e0aef60f..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ResponseCallback.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.mybmw.internal.dto.network.NetworkError; - -/** - * The {@link ResponseCallback} Marker Interface for all ASYNC REST API callbacks - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public interface ResponseCallback { - public void onError(NetworkError error); -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java deleted file mode 100644 index 80fbeea1758b5..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/StringResponseCallback.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link StringResponseCallback} Interface for all String results from ASYNC REST API - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public interface StringResponseCallback extends ResponseCallback { - - public void onResponse(@Nullable String result); -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java deleted file mode 100644 index 740be7a24e27d..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleChannelHandler.java +++ /dev/null @@ -1,478 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler; - -import static org.openhab.binding.mybmw.internal.MyBMWConstants.*; - -import java.time.DayOfWeek; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import javax.measure.Unit; -import javax.measure.quantity.Length; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType; -import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile; -import org.openhab.binding.mybmw.internal.dto.charge.ChargeSession; -import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer; -import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings; -import org.openhab.binding.mybmw.internal.dto.properties.CBS; -import org.openhab.binding.mybmw.internal.dto.properties.DoorsWindows; -import org.openhab.binding.mybmw.internal.dto.properties.Location; -import org.openhab.binding.mybmw.internal.dto.properties.Tires; -import org.openhab.binding.mybmw.internal.dto.status.CCMMessage; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; -import org.openhab.binding.mybmw.internal.utils.ChargeProfileUtils; -import org.openhab.binding.mybmw.internal.utils.ChargeProfileUtils.TimedChannel; -import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper; -import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey; -import org.openhab.binding.mybmw.internal.utils.Constants; -import org.openhab.binding.mybmw.internal.utils.Converter; -import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils; -import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils; -import org.openhab.core.i18n.LocationProvider; -import org.openhab.core.library.types.DateTimeType; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.library.types.PointType; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.types.StringType; -import org.openhab.core.library.unit.ImperialUnits; -import org.openhab.core.library.unit.SIUnits; -import org.openhab.core.library.unit.Units; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.binding.BaseThingHandler; -import org.openhab.core.types.CommandOption; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link VehicleChannelHandler} handles Channel updates - * - * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - edit & send of charge profile - */ -@NonNullByDefault -public abstract class VehicleChannelHandler extends BaseThingHandler { - protected final Logger logger = LoggerFactory.getLogger(VehicleChannelHandler.class); - protected boolean hasFuel = false; - protected boolean isElectric = false; - protected boolean isHybrid = false; - - // List Interfaces - protected List serviceList = new ArrayList(); - protected String selectedService = Constants.UNDEF; - protected List checkControlList = new ArrayList(); - protected String selectedCC = Constants.UNDEF; - protected List sessionList = new ArrayList(); - protected String selectedSession = Constants.UNDEF; - - protected MyBMWCommandOptionProvider commandOptionProvider; - private LocationProvider locationProvider; - - // Data Caches - protected Optional vehicleStatusCache = Optional.empty(); - protected Optional imageCache = Optional.empty(); - - public VehicleChannelHandler(Thing thing, MyBMWCommandOptionProvider cop, LocationProvider lp, String type) { - super(thing); - commandOptionProvider = cop; - locationProvider = lp; - if (lp.getLocation() == null) { - logger.debug("Home location not available"); - } - - hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString()) - || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.MILD_HYBRID.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 options) { - commandOptionProvider.setCommandOptions(new ChannelUID(thing.getUID(), group, id), options); - } - - protected void updateChannel(final String group, final String id, final State state) { - updateState(new ChannelUID(thing.getUID(), group, id), state); - } - - protected void updateChargeStatistics(ChargeStatisticsContainer csc) { - updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, TITLE, StringType.valueOf(csc.description)); - updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, ENERGY, - QuantityType.valueOf(csc.statistics.totalEnergyCharged, Units.KILOWATT_HOUR)); - updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, SESSIONS, - DecimalType.valueOf(Integer.toString(csc.statistics.numberOfChargingSessions))); - } - - protected void updateVehicle(Vehicle v) { - updateVehicleStatus(v); - updateRange(v); - updateDoors(v.properties.doorsAndWindows); - updateWindows(v.properties.doorsAndWindows); - updatePosition(v.properties.vehicleLocation); - updateServices(v.properties.serviceRequired); - updateCheckControls(v.status.checkControlMessages); - updateTires(v.properties.tires); - } - - private void updateTires(@Nullable Tires tires) { - if (tires == null) { - updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET, UnDefType.UNDEF); - } else { - updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT, - QuantityType.valueOf(tires.frontLeft.status.currentPressure / 100, Units.BAR)); - updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET, - QuantityType.valueOf(tires.frontLeft.status.targetPressure / 100, Units.BAR)); - updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT, - QuantityType.valueOf(tires.frontRight.status.currentPressure / 100, Units.BAR)); - updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET, - QuantityType.valueOf(tires.frontRight.status.targetPressure / 100, Units.BAR)); - updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT, - QuantityType.valueOf(tires.rearLeft.status.currentPressure / 100, Units.BAR)); - updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET, - QuantityType.valueOf(tires.rearLeft.status.targetPressure / 100, Units.BAR)); - updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT, - QuantityType.valueOf(tires.rearRight.status.currentPressure / 100, Units.BAR)); - updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET, - QuantityType.valueOf(tires.rearRight.status.targetPressure / 100, Units.BAR)); - } - } - - protected void updateVehicleStatus(Vehicle v) { - updateChannel(CHANNEL_GROUP_STATUS, LOCK, Converter.getLockState(v.properties.areDoorsLocked)); - updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE, - VehicleStatusUtils.getNextServiceDate(v.properties.serviceRequired)); - updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE, - VehicleStatusUtils.getNextServiceMileage(v.properties.serviceRequired)); - updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL, - StringType.valueOf(v.status.checkControlMessagesGeneralState)); - updateChannel(CHANNEL_GROUP_STATUS, MOTION, OnOffType.from(v.properties.inMotion)); - updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE, - DateTimeType.valueOf(Converter.zonedToLocalDateTime(v.properties.lastUpdatedAt))); - updateChannel(CHANNEL_GROUP_STATUS, DOORS, Converter.getClosedState(v.properties.areDoorsClosed)); - updateChannel(CHANNEL_GROUP_STATUS, WINDOWS, Converter.getClosedState(v.properties.areWindowsClosed)); - - if (isElectric) { - updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION, - Converter.getConnectionState(v.properties.chargingState.isChargerConnected)); - updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS, - StringType.valueOf(Converter.toTitleCase(VehicleStatusUtils.getChargStatus(v)))); - updateChannel(CHANNEL_GROUP_STATUS, CHARGE_INFO, - StringType.valueOf(Converter.getLocalTime(VehicleStatusUtils.getChargeInfo(v)))); - } - } - - protected void updateRange(Vehicle v) { - // get the right unit - Unit lengthUnit = VehicleStatusUtils.getLengthUnit(v.status.fuelIndicators); - if (lengthUnit == null) { - return; - } - if (isElectric) { - int rangeElectric = VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, v); - QuantityType qtElectricRange = QuantityType.valueOf(rangeElectric, lengthUnit); - QuantityType qtElectricRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeElectric), - lengthUnit); - updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC, qtElectricRange); - updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC, qtElectricRadius); - } - if (hasFuel) { - int rangeFuel = VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, v); - QuantityType qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit); - QuantityType qtFuelRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeFuel), lengthUnit); - updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, qtFuelRange); - updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL, qtFuelRadius); - } - if (isHybrid) { - int rangeCombined = VehicleStatusUtils.getRange(Constants.PHEV, v); - QuantityType qtHybridRange = QuantityType.valueOf(rangeCombined, lengthUnit); - QuantityType qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeCombined), - lengthUnit); - updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID, qtHybridRange); - updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID, qtHybridRadius); - } - if (v.status.currentMileage.mileage == Constants.INT_UNDEF) { - updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, UnDefType.UNDEF); - } else { - updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, - QuantityType.valueOf(v.status.currentMileage.mileage, lengthUnit)); - } - if (isElectric) { - updateChannel(CHANNEL_GROUP_RANGE, SOC, - QuantityType.valueOf(v.properties.chargingState.chargePercentage, Units.PERCENT)); - } - if (hasFuel) { - updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL, - QuantityType.valueOf(v.properties.fuelLevel.value, Units.LITRE)); - } - } - - protected void updateCheckControls(List ccl) { - if (ccl.isEmpty()) { - // No Check Control available - show not active - CCMMessage ccm = new CCMMessage(); - ccm.title = Constants.NO_ENTRIES; - ccm.longDescription = Constants.NO_ENTRIES; - ccm.state = Constants.NO_ENTRIES; - ccl.add(ccm); - } - - // add all elements to options - checkControlList = ccl; - List ccmDescriptionOptions = new ArrayList<>(); - boolean isSelectedElementIn = false; - int index = 0; - for (CCMMessage ccEntry : checkControlList) { - ccmDescriptionOptions.add(new CommandOption(Integer.toString(index), ccEntry.title)); - if (selectedCC.equals(ccEntry.title)) { - isSelectedElementIn = true; - } - index++; - } - setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions); - - // if current selected item isn't anymore in the list select first entry - if (!isSelectedElementIn) { - selectCheckControl(0); - } - } - - protected void selectCheckControl(int index) { - if (index >= 0 && index < checkControlList.size()) { - CCMMessage ccEntry = checkControlList.get(index); - selectedCC = ccEntry.title; - updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, StringType.valueOf(ccEntry.title)); - updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, StringType.valueOf(ccEntry.longDescription)); - updateChannel(CHANNEL_GROUP_CHECK_CONTROL, SEVERITY, StringType.valueOf(ccEntry.state)); - } - } - - protected void updateServices(List sl) { - // if list is empty add "undefined" element - if (sl.isEmpty()) { - CBS cbsm = new CBS(); - cbsm.type = Constants.NO_ENTRIES; - sl.add(cbsm); - } - - // add all elements to options - serviceList = sl; - List serviceNameOptions = new ArrayList<>(); - boolean isSelectedElementIn = false; - int index = 0; - for (CBS serviceEntry : serviceList) { - // create StateOption with "value = list index" and "label = human readable string" - serviceNameOptions.add(new CommandOption(Integer.toString(index), serviceEntry.type)); - if (selectedService.equals(serviceEntry.type)) { - isSelectedElementIn = true; - } - index++; - } - setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions); - - // if current selected item isn't anymore in the list select first entry - if (!isSelectedElementIn) { - selectService(0); - } - } - - protected void selectService(int index) { - if (index >= 0 && index < serviceList.size()) { - CBS serviceEntry = serviceList.get(index); - selectedService = serviceEntry.type; - updateChannel(CHANNEL_GROUP_SERVICE, NAME, StringType.valueOf(Converter.toTitleCase(serviceEntry.type))); - if (serviceEntry.dateTime != null) { - updateChannel(CHANNEL_GROUP_SERVICE, DATE, - DateTimeType.valueOf(Converter.zonedToLocalDateTime(serviceEntry.dateTime))); - } else { - updateChannel(CHANNEL_GROUP_SERVICE, DATE, UnDefType.UNDEF); - } - if (serviceEntry.distance != null) { - if (Constants.KILOMETERS_JSON.equals(serviceEntry.distance.units)) { - updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE, - QuantityType.valueOf(serviceEntry.distance.value, Constants.KILOMETRE_UNIT)); - } else { - updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE, - QuantityType.valueOf(serviceEntry.distance.value, ImperialUnits.MILE)); - } - } else { - updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE, - QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT)); - } - } - } - - protected void updateSessions(List sl) { - // if list is empty add "undefined" element - if (sl.isEmpty()) { - ChargeSession cs = new ChargeSession(); - cs.title = Constants.NO_ENTRIES; - sl.add(cs); - } - - // add all elements to options - sessionList = sl; - List sessionNameOptions = new ArrayList<>(); - boolean isSelectedElementIn = false; - int index = 0; - for (ChargeSession session : sessionList) { - // create StateOption with "value = list index" and "label = human readable string" - sessionNameOptions.add(new CommandOption(Integer.toString(index), session.title)); - if (selectedService.equals(session.title)) { - isSelectedElementIn = true; - } - index++; - } - setOptions(CHANNEL_GROUP_CHARGE_SESSION, TITLE, sessionNameOptions); - - // if current selected item isn't anymore in the list select first entry - if (!isSelectedElementIn) { - selectSession(0); - } - } - - protected void selectSession(int index) { - if (index >= 0 && index < sessionList.size()) { - ChargeSession sessionEntry = sessionList.get(index); - selectedService = sessionEntry.title; - updateChannel(CHANNEL_GROUP_CHARGE_SESSION, TITLE, StringType.valueOf(sessionEntry.title)); - updateChannel(CHANNEL_GROUP_CHARGE_SESSION, SUBTITLE, StringType.valueOf(sessionEntry.subtitle)); - if (sessionEntry.energyCharged != null) { - updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(sessionEntry.energyCharged)); - } else { - updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(Constants.UNDEF)); - } - if (sessionEntry.issues != null) { - updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(sessionEntry.issues)); - } else { - updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(Constants.HYPHEN)); - } - updateChannel(CHANNEL_GROUP_CHARGE_SESSION, STATUS, StringType.valueOf(sessionEntry.sessionStatus)); - } - } - - protected void updateChargeProfile(ChargeProfile cp) { - ChargeProfileWrapper cpw = new ChargeProfileWrapper(cp); - - updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_PREFERENCE, StringType.valueOf(cpw.getPreference())); - updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_MODE, StringType.valueOf(cpw.getMode())); - updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CONTROL, StringType.valueOf(cpw.getControlType())); - ChargingSettings cs = cpw.getChargeSettings(); - if (cs != null) { - updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_TARGET, - DecimalType.valueOf(Integer.toString(cs.targetSoc))); - updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_LIMIT, - OnOffType.from(cs.isAcCurrentLimitActive)); - } - final Boolean climate = cpw.isEnabled(ProfileKey.CLIMATE); - updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CLIMATE, - climate == null ? UnDefType.UNDEF : OnOffType.from(climate)); - updateTimedState(cpw, ProfileKey.WINDOWSTART); - updateTimedState(cpw, ProfileKey.WINDOWEND); - updateTimedState(cpw, ProfileKey.TIMER1); - updateTimedState(cpw, ProfileKey.TIMER2); - updateTimedState(cpw, ProfileKey.TIMER3); - updateTimedState(cpw, ProfileKey.TIMER4); - } - - protected void updateTimedState(ChargeProfileWrapper profile, ProfileKey key) { - final TimedChannel timed = ChargeProfileUtils.getTimedChannel(key); - if (timed != null) { - final LocalTime time = profile.getTime(key); - updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.time, - time.equals(Constants.NULL_LOCAL_TIME) ? UnDefType.UNDEF - : new DateTimeType(ZonedDateTime.of(Constants.EPOCH_DAY, time, ZoneId.systemDefault()))); - if (timed.timer != null) { - final Boolean enabled = profile.isEnabled(key); - updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.timer + CHARGE_ENABLED, - enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled)); - if (timed.hasDays) { - final Set days = profile.getDays(key); - EnumSet.allOf(DayOfWeek.class).forEach(day -> { - updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, - timed.timer + ChargeProfileUtils.getDaysChannel(day), - days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day))); - }); - } - } - } - } - - protected void updateDoors(DoorsWindows dw) { - updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT, - StringType.valueOf(Converter.toTitleCase(dw.doors.driverFront))); - updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR, - StringType.valueOf(Converter.toTitleCase(dw.doors.driverRear))); - updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT, - StringType.valueOf(Converter.toTitleCase(dw.doors.passengerFront))); - updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR, - StringType.valueOf(Converter.toTitleCase(dw.doors.passengerRear))); - updateChannel(CHANNEL_GROUP_DOORS, TRUNK, StringType.valueOf(Converter.toTitleCase(dw.trunk))); - updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(dw.hood))); - } - - protected void updateWindows(DoorsWindows dw) { - updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT, - StringType.valueOf(Converter.toTitleCase(dw.windows.driverFront))); - updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR, - StringType.valueOf(Converter.toTitleCase(dw.windows.driverRear))); - updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT, - StringType.valueOf(Converter.toTitleCase(dw.windows.passengerFront))); - updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR, - StringType.valueOf(Converter.toTitleCase(dw.windows.passengerRear))); - updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, StringType.valueOf(Converter.toTitleCase(dw.moonroof))); - } - - protected void updatePosition(Location pos) { - if (pos.coordinates.latitude < 0) { - updateChannel(CHANNEL_GROUP_LOCATION, GPS, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_LOCATION, HEADING, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, UnDefType.UNDEF); - updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, UnDefType.UNDEF); - } else { - PointType vehicleLocation = PointType.valueOf( - Double.toString(pos.coordinates.latitude) + "," + Double.toString(pos.coordinates.longitude)); - updateChannel(CHANNEL_GROUP_LOCATION, GPS, vehicleLocation); - updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE)); - updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, StringType.valueOf(pos.address.formatted)); - PointType homeLocation = locationProvider.getLocation(); - if (homeLocation != null) { - updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, - QuantityType.valueOf(vehicleLocation.distanceFrom(homeLocation).intValue(), SIUnits.METRE)); - } else { - updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, UnDefType.UNDEF); - } - } - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java index 81c0b67ada63a..19fb1829cd54c 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java @@ -12,167 +12,236 @@ */ package org.openhab.binding.mybmw.internal.handler; -import static org.openhab.binding.mybmw.internal.MyBMWConstants.*; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.ADDRESS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHARGE_PROFILE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHARGE_SESSION; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHARGE_STATISTICS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHECK_CONTROL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_DOORS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_LOCATION; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_RANGE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_REMOTE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_SERVICE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_STATUS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_TIRES; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_VEHICLE_IMAGE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_ENABLED; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_CLIMATE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_CONTROL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_LIMIT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_MODE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_PREFERENCE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_TARGET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_REMAINING; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_STATUS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHECK_CONTROL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DATE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DETAILS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOORS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_DRIVER_FRONT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_DRIVER_REAR; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_PASSENGER_FRONT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_PASSENGER_REAR; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.ENERGY; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.ESTIMATED_FUEL_L_100KM; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.ESTIMATED_FUEL_MPG; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_LEFT_CURRENT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_LEFT_TARGET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_RIGHT_CURRENT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_RIGHT_TARGET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.GPS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.HEADING; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.HOME_DISTANCE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.HOOD; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.IMAGE_FORMAT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.IMAGE_VIEWPORT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.ISSUE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.LAST_FETCHED; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.LAST_UPDATE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.LOCK; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.MILEAGE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.NAME; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.PLUG_CONNECTION; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_ELECTRIC; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_FUEL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_HYBRID; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_RADIUS_ELECTRIC; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_RADIUS_FUEL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_RADIUS_HYBRID; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RAW; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_LEFT_CURRENT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_LEFT_TARGET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_RIGHT_CURRENT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_RIGHT_TARGET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMAINING_FUEL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_COMMAND; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_STATE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SERVICE_DATE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SERVICE_MILEAGE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SESSIONS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SEVERITY; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SOC; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.STATUS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUBTITLE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUNROOF; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.TITLE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.TRUNK; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOWS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_DRIVER_FRONT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_DRIVER_REAR; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_PASSENGER_FRONT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_PASSENGER_REAR; +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 java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import javax.measure.Unit; +import javax.measure.quantity.Length; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mybmw.internal.VehicleConfiguration; -import org.openhab.binding.mybmw.internal.dto.charge.ChargeSessionsContainer; -import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer; -import org.openhab.binding.mybmw.internal.dto.network.NetworkError; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType; +import org.openhab.binding.mybmw.internal.MyBMWVehicleConfiguration; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingProfile; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSession; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.CheckControlMessage; +import org.openhab.binding.mybmw.internal.dto.vehicle.RequiredService; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleDoorsState; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleLocation; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleRoofState; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleState; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleTireStates; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleWindowsState; +import org.openhab.binding.mybmw.internal.handler.backend.MyBMWProxy; +import org.openhab.binding.mybmw.internal.handler.backend.NetworkException; +import org.openhab.binding.mybmw.internal.utils.ChargingProfileUtils; +import org.openhab.binding.mybmw.internal.utils.ChargingProfileUtils.TimedChannel; +import org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper; +import org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper.ProfileKey; import org.openhab.binding.mybmw.internal.utils.Constants; import org.openhab.binding.mybmw.internal.utils.Converter; import org.openhab.binding.mybmw.internal.utils.ImageProperties; import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils; +import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils; import org.openhab.core.i18n.LocationProvider; 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.PointType; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.RawType; import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BridgeHandler; import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; import org.openhab.core.types.RefreshType; - -import com.google.gson.JsonSyntaxException; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link VehicleHandler} handles responses from BMW API * + * the introduction of channelToBeUpdated is ugly, but if there is a refresh of one channel, always all channels were + * updated + * * @author Bernd Weymann - Initial contribution * @author Norbert Truchsess - edit & send charge profile + * @author Martin Grassl - refactoring, merge with VehicleChannelHandler + * @author Mark Herwege - refactoring, V2 API charging */ @NonNullByDefault -public class VehicleHandler extends VehicleChannelHandler { +public class VehicleHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class); + + private boolean hasFuel = false; + private boolean isElectric = false; + private boolean isHybrid = false; + + // List Interfaces + private volatile List serviceList = List.of(); + private volatile String selectedService = Constants.UNDEF; + private volatile List checkControlList = List.of(); + private volatile String selectedCC = Constants.UNDEF; + private volatile List sessionList = List.of(); + private volatile String selectedSession = Constants.UNDEF; + + private MyBMWCommandOptionProvider commandOptionProvider; + private LocationProvider locationProvider; + + // Data Caches + private Optional vehicleStatusCache = Optional.empty(); + private Optional imageCache = Optional.empty(); + private Optional proxy = Optional.empty(); - private Optional remote = Optional.empty(); - public Optional configuration = Optional.empty(); + private Optional remote = Optional.empty(); + private Optional vehicleConfiguration = Optional.empty(); private Optional> refreshJob = Optional.empty(); private Optional> editTimeout = Optional.empty(); private ImageProperties imageProperties = new ImageProperties(); - VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback(); - ChargeStatisticsCallback chargeStatisticsCallback = new ChargeStatisticsCallback(); - ChargeSessionsCallback chargeSessionCallback = new ChargeSessionsCallback(); - ByteResponseCallback imageCallback = new ImageCallback(); public VehicleHandler(Thing thing, MyBMWCommandOptionProvider cop, LocationProvider lp, String driveTrain) { - super(thing, cop, lp, driveTrain); - } + super(thing); + logger.trace("xxxVehicleHandler.constructor {}, {}", thing.getUID(), driveTrain); + commandOptionProvider = cop; + locationProvider = lp; + if (lp.getLocation() == null) { + logger.debug("Home location not available"); + } - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - String group = channelUID.getGroupId(); + hasFuel = driveTrain.equals(VehicleType.CONVENTIONAL.toString()) + || driveTrain.equals(VehicleType.PLUGIN_HYBRID.toString()) + || driveTrain.equals(VehicleType.ELECTRIC_REX.toString()) + || driveTrain.equals(VehicleType.MILD_HYBRID.toString()); + isElectric = driveTrain.equals(VehicleType.PLUGIN_HYBRID.toString()) + || driveTrain.equals(VehicleType.ELECTRIC_REX.toString()) + || driveTrain.equals(VehicleType.ELECTRIC.toString()); + isHybrid = hasFuel && isElectric; - // Refresh of Channels with cached values - if (command instanceof RefreshType) { - if (CHANNEL_GROUP_STATUS.equals(group)) { - vehicleStatusCache.ifPresent(vehicleStatus -> vehicleStatusCallback.onResponse(vehicleStatus)); - } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) { - imageCache.ifPresent(image -> imageCallback.onResponse(image)); - } - // Check for Channel Group and corresponding Actions - } else if (CHANNEL_GROUP_REMOTE.equals(group)) { - // Executing Remote Services - if (command instanceof StringType) { - String serviceCommand = ((StringType) command).toFullString(); - remote.ifPresent(remot -> { - switch (serviceCommand) { - case REMOTE_SERVICE_LIGHT_FLASH: - case REMOTE_SERVICE_DOOR_LOCK: - case REMOTE_SERVICE_DOOR_UNLOCK: - case REMOTE_SERVICE_HORN: - case REMOTE_SERVICE_VEHICLE_FINDER: - RemoteServiceUtils.getRemoteService(serviceCommand) - .ifPresentOrElse(service -> remot.execute(service), () -> { - logger.debug("Remote service execution {} unknown", serviceCommand); - }); - break; - case REMOTE_SERVICE_AIR_CONDITIONING_START: - RemoteServiceUtils.getRemoteService(serviceCommand) - .ifPresentOrElse(service -> remot.execute(service), () -> { - logger.debug("Remote service execution {} unknown", serviceCommand); - }); - break; - case REMOTE_SERVICE_AIR_CONDITIONING_STOP: - RemoteServiceUtils.getRemoteService(serviceCommand) - .ifPresentOrElse(service -> remot.execute(service), () -> { - logger.debug("Remote service execution {} unknown", serviceCommand); - }); - break; - default: - logger.debug("Remote service execution {} unknown", serviceCommand); - break; - } - }); - } - } else if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) { - // Image Change - configuration.ifPresent(config -> { - if (command instanceof StringType) { - if (channelUID.getIdWithoutGroup().equals(IMAGE_VIEWPORT)) { - String newViewport = command.toString(); - synchronized (imageProperties) { - if (!imageProperties.viewport.equals(newViewport)) { - imageProperties = new ImageProperties(newViewport); - imageCache = Optional.empty(); - proxy.ifPresent(prox -> prox.requestImage(config, imageProperties, imageCallback)); - } - } - updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport)); - } - } - }); - } else if (CHANNEL_GROUP_SERVICE.equals(group)) { - if (command instanceof StringType) { - int index = Converter.getIndex(command.toFullString()); - if (index != -1) { - selectService(index); - } else { - logger.debug("Cannot select Service index {}", command.toFullString()); - } - } - } else if (CHANNEL_GROUP_CHECK_CONTROL.equals(group)) { - if (command instanceof StringType) { - int index = Converter.getIndex(command.toFullString()); - if (index != -1) { - selectCheckControl(index); - } else { - logger.debug("Cannot select CheckControl index {}", command.toFullString()); - } - } - } else if (CHANNEL_GROUP_CHARGE_SESSION.equals(group)) { - if (command instanceof StringType) { - int index = Converter.getIndex(command.toFullString()); - if (index != -1) { - selectSession(index); - } else { - logger.debug("Cannot select Session index {}", command.toFullString()); - } - } - } + setOptions(CHANNEL_GROUP_REMOTE, REMOTE_SERVICE_COMMAND, RemoteServiceUtils.getOptions(isElectric)); + } + + private void setOptions(final String group, final String id, List options) { + commandOptionProvider.setCommandOptions(new ChannelUID(thing.getUID(), group, id), options); } @Override public void initialize() { + logger.trace("xxxVehicleHandler.initialize"); updateStatus(ThingStatus.UNKNOWN); - final VehicleConfiguration config = getConfigAs(VehicleConfiguration.class); - configuration = Optional.of(config); + vehicleConfiguration = Optional.of(getConfigAs(MyBMWVehicleConfiguration.class)); + Bridge bridge = getBridge(); if (bridge != null) { BridgeHandler handler = bridge.getHandler(); if (handler != null) { - proxy = ((MyBMWBridgeHandler) handler).getProxy(); - remote = proxy.map(prox -> prox.getRemoteServiceHandler(this)); + proxy = ((MyBMWBridgeHandler) handler).getMyBmwProxy(); + remote = Optional.of(new RemoteServiceExecutor(this, proxy.get())); } else { logger.debug("Bridge Handler null"); } @@ -181,13 +250,15 @@ public void initialize() { } imageProperties = new ImageProperties(); - updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(imageProperties.viewport)); + updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, Converter.toTitleCase(imageProperties.viewport), + null); // start update schedule - startSchedule(config.refreshInterval); + startSchedule(vehicleConfiguration.get().getRefreshInterval()); } private void startSchedule(int interval) { + logger.trace("xxxVehicleHandler.startSchedule"); refreshJob.ifPresentOrElse(job -> { if (job.isCancelled()) { refreshJob = Optional @@ -200,21 +271,46 @@ private void startSchedule(int interval) { @Override public void dispose() { + logger.trace("xxxVehicleHandler.idispose"); refreshJob.ifPresent(job -> job.cancel(true)); editTimeout.ifPresent(job -> job.cancel(true)); - remote.ifPresent(RemoteServiceHandler::cancel); + remote.ifPresent(RemoteServiceExecutor::cancel); } public void getData() { + logger.trace("xxxVehicleHandler.getData"); proxy.ifPresentOrElse(prox -> { - configuration.ifPresentOrElse(config -> { - prox.requestVehicles(config.vehicleBrand, vehicleStatusCallback); - if (isElectric) { - prox.requestChargeStatistics(config, chargeStatisticsCallback); - prox.requestChargeSessions(config, chargeSessionCallback); + vehicleConfiguration.ifPresentOrElse(config -> { + + boolean stateError = false; + try { + VehicleStateContainer vehicleState = prox.requestVehicleState(config.getVin(), + config.getVehicleBrand()); + triggerVehicleStatusUpdate(vehicleState, null); + stateError = false; + } catch (NetworkException e) { + logger.debug("{}", e.toString()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Vehicle State Update failed"); + stateError = true; } - if (!imageCache.isPresent() && !imageProperties.failLimitReached()) { - prox.requestImage(config, imageProperties, imageCallback); + + if (!stateError && isElectric) { + try { + updateChargingStatistics( + prox.requestChargeStatistics(config.getVin(), config.getVehicleBrand()), null); + updateChargingSessions(prox.requestChargeSessions(config.getVin(), config.getVehicleBrand()), + null); + } catch (NetworkException e) { + logger.debug("{}", e.toString()); + } + } + if (!stateError && !imageCache.isPresent() && !imageProperties.failLimitReached()) { + try { + updateImage(prox.requestImage(config.getVin(), config.getVehicleBrand(), imageProperties)); + } catch (NetworkException e) { + logger.debug("{}", e.toString()); + } } }, () -> { logger.warn("MyBMW Vehicle Configuration isn't present"); @@ -224,129 +320,644 @@ public void getData() { }); } + private void triggerVehicleStatusUpdate(VehicleStateContainer vehicleState, @Nullable String channelToBeUpdated) { + logger.trace("xxxVehicleHandler.triggerVehicleStatusUpdate for {}", channelToBeUpdated); + if (vehicleConfiguration.isPresent()) { + vehicleStatusCache = Optional.of(vehicleState); + updateChannel(CHANNEL_GROUP_STATUS, RAW, vehicleState.getRawStateJson(), channelToBeUpdated); + + updateVehicleStatus(vehicleState.getState(), channelToBeUpdated); + if (isElectric) { + updateChargingProfile(vehicleState.getState().getChargingProfile(), channelToBeUpdated); + } + + updateStatus(ThingStatus.ONLINE); + } else { + logger.debug("configuration not present"); + } + } + public void updateRemoteExecutionStatus(@Nullable String service, String status) { updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE, - StringType.valueOf((service == null ? "-" : service) + Constants.SPACE + status.toLowerCase())); + (service == null ? "-" : service) + Constants.SPACE + status.toLowerCase(), null); } - public Optional getConfiguration() { - return configuration; + public Optional getVehicleConfiguration() { + logger.trace("xxxVehicleHandler.getVehicleConfiguration"); + return vehicleConfiguration; } public ScheduledExecutorService getScheduler() { + logger.trace("xxxVehicleHandler.getScheduler"); return scheduler; } - public class ImageCallback implements ByteResponseCallback { - @Override - public void onResponse(byte[] content) { - if (content.length > 0) { - imageCache = Optional.of(content); - String contentType = HttpUtil.guessContentTypeFromData(content); - updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_FORMAT, new RawType(content, contentType)); + private void updateChannel(final String group, final String id, final String state, + @Nullable final String channelToBeUpdated) { + updateChannel(group, id, StringType.valueOf(state), channelToBeUpdated); + } + + /** + * this method sets the state for a single channel. if a channelToBeUpdated is provided, the update will only take + * place for that + * single channel + */ + private void updateChannel(final String group, final String id, final State state, + @Nullable final String channelToBeUpdated) { + if (channelToBeUpdated == null || id.equals(channelToBeUpdated)) { + if (!"png".equals(id)) { + logger.trace("updating channel {}, {}, {}", group, id, state.toFullString()); } else { - synchronized (imageProperties) { - imageProperties.failed(); - } + logger.trace("updating channel {}, {}, {}", group, id, "not printed"); } + + updateState(new ChannelUID(thing.getUID(), group, id), state); } + } - /** - * 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(); - } + private void updateChargingStatistics(ChargingStatisticsContainer chargingStatisticsContainer, + @Nullable String channelToBeUpdated) { + if (!"".equals(chargingStatisticsContainer.getDescription())) { + updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, TITLE, chargingStatisticsContainer.getDescription(), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, ENERGY, QuantityType + .valueOf(chargingStatisticsContainer.getStatistics().getTotalEnergyCharged(), Units.KILOWATT_HOUR), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, SESSIONS, + DecimalType.valueOf(Integer + .toString(chargingStatisticsContainer.getStatistics().getNumberOfChargingSessions())), + channelToBeUpdated); } } /** - * The VehicleStatus is supported by all Vehicle Types so it's used to reflect the Thing Status + * updates the channels with the current state of the vehicle + * + * @param vehicleStateState */ - public class VehicleStatusCallback implements StringResponseCallback { - @Override - public void onResponse(@Nullable String content) { - if (content != null) { - if (getConfiguration().isPresent()) { - Vehicle v = Converter.getVehicle(configuration.get().vin, content); - if (v.valid) { - vehicleStatusCache = Optional.of(content); - updateStatus(ThingStatus.ONLINE); - updateChannel(CHANNEL_GROUP_STATUS, RAW, - StringType.valueOf(Converter.getRawVehicleContent(configuration.get().vin, content))); - updateVehicle(v); - if (isElectric) { - updateChargeProfile(v.status.chargingProfile); - } - } else { - logger.debug("Vehicle {} not valid", configuration.get().vin); - } - } else { - logger.debug("configuration not present"); - } + private void updateVehicleStatus(VehicleState vehicleStateState, @Nullable String channelToBeUpdated) { + boolean isLeftSteering = vehicleStateState.isLeftSteering(); + + updateVehicleOverallStatus(vehicleStateState, channelToBeUpdated); + updateRange(vehicleStateState, channelToBeUpdated); + updateDoors(vehicleStateState.getDoorsState(), isLeftSteering, channelToBeUpdated); + updateWindows(vehicleStateState.getWindowsState(), isLeftSteering, channelToBeUpdated); + updateRoof(vehicleStateState.getRoofState(), channelToBeUpdated); + updatePosition(vehicleStateState.getLocation(), channelToBeUpdated); + updateServices(vehicleStateState.getRequiredServices(), channelToBeUpdated); + updateCheckControls(vehicleStateState.getCheckControlMessages(), channelToBeUpdated); + updateTires(vehicleStateState.getTireState(), channelToBeUpdated); + } + + private void updateTires(@Nullable VehicleTireStates vehicleTireStates, @Nullable String channelToBeUpdated) { + if (vehicleTireStates == null) { + updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET, UnDefType.UNDEF, channelToBeUpdated); + } else { + updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT, + calculatePressure(vehicleTireStates.getFrontLeft().getStatus().getCurrentPressure()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET, + calculatePressure(vehicleTireStates.getFrontLeft().getStatus().getTargetPressure()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT, + calculatePressure(vehicleTireStates.getFrontRight().getStatus().getCurrentPressure()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET, + calculatePressure(vehicleTireStates.getFrontRight().getStatus().getTargetPressure()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT, + calculatePressure(vehicleTireStates.getRearLeft().getStatus().getCurrentPressure()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET, + calculatePressure(vehicleTireStates.getRearLeft().getStatus().getTargetPressure()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT, + calculatePressure(vehicleTireStates.getRearRight().getStatus().getCurrentPressure()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET, + calculatePressure(vehicleTireStates.getRearRight().getStatus().getTargetPressure()), + channelToBeUpdated); + } + } + + /** + * if the pressure is undef it is < 0 + * + * @param pressure + * @return + */ + private State calculatePressure(int pressure) { + if (pressure > 0) { + return QuantityType.valueOf(pressure / 100.0, Units.BAR); + } else { + return UnDefType.UNDEF; + } + } + + private void updateVehicleOverallStatus(VehicleState vehicleState, @Nullable String channelToBeUpdated) { + updateChannel(CHANNEL_GROUP_STATUS, LOCK, + Converter.toTitleCase(vehicleState.getDoorsState().getCombinedSecurityState()), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE, + VehicleStatusUtils.getNextServiceDate(vehicleState.getRequiredServices()), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE, + VehicleStatusUtils.getNextServiceMileage(vehicleState.getRequiredServices()), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL, + Converter.toTitleCase(vehicleState.getOverallCheckControlStatus()), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE, + Converter.zonedToLocalDateTime(vehicleState.getLastUpdatedAt()), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_STATUS, LAST_FETCHED, Converter.zonedToLocalDateTime(vehicleState.getLastFetched()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_STATUS, DOORS, + Converter.toTitleCase(vehicleState.getDoorsState().getCombinedState()), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_STATUS, WINDOWS, + Converter.toTitleCase(vehicleState.getWindowsState().getCombinedState()), channelToBeUpdated); + + if (isElectric) { + updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION, + Converter.getConnectionState(vehicleState.getElectricChargingState().isChargerConnected()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS, + Converter.toTitleCase(vehicleState.getElectricChargingState().getChargingStatus()), + channelToBeUpdated); + + int remainingTime = vehicleState.getElectricChargingState().getRemainingChargingMinutes(); + updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING, + remainingTime >= 0 ? QuantityType.valueOf(remainingTime, Units.MINUTE) : UnDefType.UNDEF, + channelToBeUpdated); + } + } + + private void updateRange(VehicleState vehicleState, @Nullable String channelToBeUpdated) { + // get the right unit + Unit lengthUnit = Constants.KILOMETRE_UNIT; + + if (isElectric) { + int rangeElectric = vehicleState.getElectricChargingState().getRange(); + QuantityType qtElectricRange = QuantityType.valueOf(rangeElectric, lengthUnit); + QuantityType qtElectricRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeElectric), + lengthUnit); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC, qtElectricRange, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC, qtElectricRadius, channelToBeUpdated); + } + + if (hasFuel && !isHybrid) { + int rangeFuel = vehicleState.getCombustionFuelLevel().getRange(); + QuantityType qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit); + QuantityType qtFuelRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeFuel), lengthUnit); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, qtFuelRange, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL, qtFuelRadius, channelToBeUpdated); + } + + if (isHybrid) { + int rangeCombined = vehicleState.getRange(); + + // there is a bug/feature in the API that the fuel range is the same like the combined range, hence in case + // of + // hybrid the fuel range has to be subtracted by the electric range + int rangeFuel = vehicleState.getCombustionFuelLevel().getRange() + - vehicleState.getElectricChargingState().getRange(); + + QuantityType qtHybridRange = QuantityType.valueOf(rangeCombined, lengthUnit); + QuantityType qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeCombined), + lengthUnit); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID, qtHybridRange, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID, qtHybridRadius, channelToBeUpdated); + + QuantityType qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit); + QuantityType qtFuelRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeFuel), lengthUnit); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, qtFuelRange, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL, qtFuelRadius, channelToBeUpdated); + } + + if (vehicleState.getCurrentMileage() == Constants.INT_UNDEF) { + updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, UnDefType.UNDEF, channelToBeUpdated); + } else { + updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, + QuantityType.valueOf(vehicleState.getCurrentMileage(), lengthUnit), channelToBeUpdated); + } + if (isElectric) { + updateChannel( + CHANNEL_GROUP_RANGE, SOC, QuantityType + .valueOf(vehicleState.getElectricChargingState().getChargingLevelPercent(), Units.PERCENT), + channelToBeUpdated); + } + if (hasFuel) { + updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL, + QuantityType.valueOf(vehicleState.getCombustionFuelLevel().getRemainingFuelLiters(), Units.LITRE), + channelToBeUpdated); + + if (vehicleState.getCombustionFuelLevel().getRemainingFuelLiters() > 0 + && vehicleState.getCombustionFuelLevel().getRange() > 1) { + double estimatedFuelConsumption = vehicleState.getCombustionFuelLevel().getRemainingFuelLiters() * 1.0 + / vehicleState.getCombustionFuelLevel().getRange() * 100.0; + updateChannel(CHANNEL_GROUP_RANGE, ESTIMATED_FUEL_L_100KM, + DecimalType.valueOf(estimatedFuelConsumption + ""), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_RANGE, ESTIMATED_FUEL_MPG, + DecimalType.valueOf((235.214583 / estimatedFuelConsumption) + ""), channelToBeUpdated); } else { - updateChannel(CHANNEL_GROUP_STATUS, RAW, StringType.valueOf(Constants.EMPTY_JSON)); - logger.debug("Content not valid"); + updateChannel(CHANNEL_GROUP_RANGE, ESTIMATED_FUEL_L_100KM, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_RANGE, ESTIMATED_FUEL_MPG, UnDefType.UNDEF, channelToBeUpdated); + } + } + } + + private void updateCheckControls(List checkControlMessages, + @Nullable String channelToBeUpdated) { + if (checkControlMessages.isEmpty()) { + // No Check Control available - show not active + CheckControlMessage checkControlMessage = new CheckControlMessage(); + checkControlMessage.setName(Constants.NO_ENTRIES); + checkControlMessage.setDescription(Constants.NO_ENTRIES); + checkControlMessage.setSeverity(Constants.NO_ENTRIES); + checkControlMessage.setType(Constants.NO_ENTRIES); + checkControlMessage.setId(-1); + checkControlMessages.add(checkControlMessage); + } + + // add all elements to options + checkControlList = checkControlMessages; + List ccmDescriptionOptions = new ArrayList<>(); + boolean isSelectedElementIn = false; + int index = 0; + for (CheckControlMessage checkControlMessage : checkControlList) { + ccmDescriptionOptions.add( + new CommandOption(Integer.toString(index), Converter.toTitleCase(checkControlMessage.getType()))); + if (selectedCC.equals(checkControlMessage.getType())) { + isSelectedElementIn = true; } + index++; } + setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions); - @Override - public void onError(NetworkError error) { - logger.debug("{}", error.toString()); - vehicleStatusCache = Optional.of(Converter.getGson().toJson(error)); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason); + // if current selected item isn't anymore in the list select first entry + if (!isSelectedElementIn) { + selectCheckControl(0, channelToBeUpdated); } } - public class ChargeStatisticsCallback implements StringResponseCallback { - @Override - public void onResponse(@Nullable String content) { - if (content != null) { - try { - ChargeStatisticsContainer csc = Converter.getGson().fromJson(content, - ChargeStatisticsContainer.class); - if (csc != null) { - updateChargeStatistics(csc); - } - } catch (JsonSyntaxException jse) { - logger.warn("{}", jse.getLocalizedMessage()); - } + private void selectCheckControl(int index, @Nullable String channelToBeUpdated) { + if (index >= 0 && index < checkControlList.size()) { + CheckControlMessage checkControlMessage = checkControlList.get(index); + selectedCC = checkControlMessage.getType(); + updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, Converter.toTitleCase(checkControlMessage.getType()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, + StringType.valueOf(checkControlMessage.getDescription()), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_CHECK_CONTROL, SEVERITY, + Converter.toTitleCase(checkControlMessage.getSeverity()), channelToBeUpdated); + } + } + + private void updateServices(List requiredServiceList, @Nullable String channelToBeUpdated) { + // if list is empty add "undefined" element + if (requiredServiceList.isEmpty()) { + RequiredService requiredService = new RequiredService(); + requiredService.setType(Constants.NO_ENTRIES); + requiredService.setDescription(Constants.NO_ENTRIES); + requiredServiceList.add(requiredService); + } + + // add all elements to options + serviceList = requiredServiceList; + List serviceNameOptions = new ArrayList<>(); + boolean isSelectedElementIn = false; + int index = 0; + for (RequiredService requiredService : requiredServiceList) { + // create StateOption with "value = list index" and "label = human readable + // string" + serviceNameOptions + .add(new CommandOption(Integer.toString(index), Converter.toTitleCase(requiredService.getType()))); + if (selectedService.equals(requiredService.getType())) { + isSelectedElementIn = true; + } + index++; + } + + setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions); + + // if current selected item isn't anymore in the list select first entry + if (!isSelectedElementIn) { + selectService(0, channelToBeUpdated); + } + } + + private void selectService(int index, @Nullable String channelToBeUpdated) { + if (index >= 0 && index < serviceList.size()) { + RequiredService serviceEntry = serviceList.get(index); + selectedService = serviceEntry.getType(); + updateChannel(CHANNEL_GROUP_SERVICE, NAME, Converter.toTitleCase(serviceEntry.getType()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_SERVICE, DETAILS, StringType.valueOf(serviceEntry.getDescription()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_SERVICE, DATE, Converter.zonedToLocalDateTime(serviceEntry.getDateTime()), + channelToBeUpdated); + + if (serviceEntry.getMileage() > 0) { + updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE, + QuantityType.valueOf(serviceEntry.getMileage(), Constants.KILOMETRE_UNIT), channelToBeUpdated); } else { - logger.debug("Content not valid"); + updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE, UnDefType.UNDEF, channelToBeUpdated); + } + } + } + + private void updateChargingSessions(ChargingSessionsContainer chargeSessionsContainer, + @Nullable String channelToBeUpdated) { + List chargeSessions = new ArrayList<>(); + + if (chargeSessionsContainer.chargingSessions != null + && chargeSessionsContainer.chargingSessions.getSessions() != null + && !chargeSessionsContainer.chargingSessions.getSessions().isEmpty()) { + chargeSessions.addAll(chargeSessionsContainer.chargingSessions.getSessions()); + } else { + // if list is empty add "undefined" element + ChargingSession cs = new ChargingSession(); + cs.setTitle(Constants.NO_ENTRIES); + chargeSessions.add(cs); + } + + // add all elements to options + sessionList = chargeSessions; + List sessionNameOptions = new ArrayList<>(); + boolean isSelectedElementIn = false; + int index = 0; + for (ChargingSession session : sessionList) { + // create StateOption with "value = list index" and "label = human readable + // string" + sessionNameOptions.add(new CommandOption(Integer.toString(index), session.getTitle())); + if (selectedSession.equals(session.getTitle())) { + isSelectedElementIn = true; } + index++; } + setOptions(CHANNEL_GROUP_CHARGE_SESSION, TITLE, sessionNameOptions); - @Override - public void onError(NetworkError error) { - logger.debug("{}", error.toString()); + // if current selected item isn't anymore in the list select first entry + if (!isSelectedElementIn) { + selectSession(0, channelToBeUpdated); } } - public class ChargeSessionsCallback implements StringResponseCallback { - @Override - public void onResponse(@Nullable String content) { - if (content != null) { - try { - ChargeSessionsContainer csc = Converter.getGson().fromJson(content, ChargeSessionsContainer.class); - if (csc != null) { - if (csc.chargingSessions != null) { - updateSessions(csc.chargingSessions.sessions); - } - } - } catch (JsonSyntaxException jse) { - logger.warn("{}", jse.getLocalizedMessage()); + private void selectSession(int index, @Nullable String channelToBeUpdated) { + if (index >= 0 && index < sessionList.size()) { + ChargingSession sessionEntry = sessionList.get(index); + selectedSession = sessionEntry.getTitle(); + updateChannel(CHANNEL_GROUP_CHARGE_SESSION, TITLE, StringType.valueOf(sessionEntry.getTitle()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_CHARGE_SESSION, SUBTITLE, StringType.valueOf(sessionEntry.getSubtitle()), + channelToBeUpdated); + if (sessionEntry.getEnergyCharged() != null) { + updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(sessionEntry.getEnergyCharged()), + channelToBeUpdated); + } else { + updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(Constants.UNDEF), + channelToBeUpdated); + } + if (sessionEntry.getIssues() != null) { + updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(sessionEntry.getIssues()), + channelToBeUpdated); + } else { + updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(Constants.HYPHEN), + channelToBeUpdated); + } + updateChannel(CHANNEL_GROUP_CHARGE_SESSION, STATUS, StringType.valueOf(sessionEntry.getSessionStatus()), + channelToBeUpdated); + } + } + + private void updateChargingProfile(ChargingProfile cp, @Nullable String channelToBeUpdated) { + ChargingProfileWrapper cpw = new ChargingProfileWrapper(cp); + + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_PREFERENCE, StringType.valueOf(cpw.getPreference()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_MODE, StringType.valueOf(cpw.getMode()), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CONTROL, StringType.valueOf(cpw.getControlType()), + channelToBeUpdated); + ChargingSettings cs = cpw.getChargingSettings(); + if (cs != null) { + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_TARGET, + DecimalType.valueOf(Integer.toString(cs.getTargetSoc())), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_LIMIT, + OnOffType.from(cs.isAcCurrentLimitActive()), channelToBeUpdated); + } + final Boolean climate = cpw.isEnabled(ProfileKey.CLIMATE); + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CLIMATE, + climate == null ? UnDefType.UNDEF : OnOffType.from(climate), channelToBeUpdated); + updateTimedState(cpw, ProfileKey.WINDOWSTART, channelToBeUpdated); + updateTimedState(cpw, ProfileKey.WINDOWEND, channelToBeUpdated); + updateTimedState(cpw, ProfileKey.TIMER1, channelToBeUpdated); + updateTimedState(cpw, ProfileKey.TIMER2, channelToBeUpdated); + updateTimedState(cpw, ProfileKey.TIMER3, channelToBeUpdated); + updateTimedState(cpw, ProfileKey.TIMER4, channelToBeUpdated); + } + + private void updateTimedState(ChargingProfileWrapper profile, ProfileKey key, @Nullable String channelToBeUpdated) { + final TimedChannel timed = ChargingProfileUtils.getTimedChannel(key); + if (timed != null) { + final LocalTime time = profile.getTime(key); + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.time, + time.equals(Constants.NULL_LOCAL_TIME) ? UnDefType.UNDEF + : new DateTimeType(ZonedDateTime.of(Constants.EPOCH_DAY, time, ZoneId.systemDefault())), + channelToBeUpdated); + if (timed.timer != null) { + final Boolean enabled = profile.isEnabled(key); + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.timer + CHARGE_ENABLED, + enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled), channelToBeUpdated); + if (timed.hasDays) { + final Set days = profile.getDays(key); + EnumSet.allOf(DayOfWeek.class).forEach(day -> { + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, + timed.timer + ChargingProfileUtils.getDaysChannel(day), + days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)), + channelToBeUpdated); + }); } + } + } + } + + private void updateDoors(VehicleDoorsState vehicleDoorsState, boolean isLeftSteering, + @Nullable String channelToBeUpdated) { + updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT, + StringType.valueOf(Converter.toTitleCase( + isLeftSteering ? vehicleDoorsState.getLeftFront() : vehicleDoorsState.getRightFront())), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR, + StringType.valueOf(Converter.toTitleCase( + isLeftSteering ? vehicleDoorsState.getLeftRear() : vehicleDoorsState.getRightRear())), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT, + StringType.valueOf(Converter.toTitleCase( + isLeftSteering ? vehicleDoorsState.getRightFront() : vehicleDoorsState.getLeftFront())), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR, + StringType.valueOf(Converter.toTitleCase( + isLeftSteering ? vehicleDoorsState.getRightRear() : vehicleDoorsState.getLeftRear())), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_DOORS, TRUNK, + StringType.valueOf(Converter.toTitleCase(vehicleDoorsState.getTrunk())), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(vehicleDoorsState.getHood())), + channelToBeUpdated); + } + + private void updateWindows(VehicleWindowsState vehicleWindowState, boolean isLeftSteering, + @Nullable String channelToBeUpdated) { + updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT, + StringType.valueOf(Converter.toTitleCase( + isLeftSteering ? vehicleWindowState.getLeftFront() : vehicleWindowState.getRightFront())), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR, + StringType.valueOf(Converter.toTitleCase( + isLeftSteering ? vehicleWindowState.getLeftRear() : vehicleWindowState.getRightRear())), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT, + StringType.valueOf(Converter.toTitleCase( + isLeftSteering ? vehicleWindowState.getRightFront() : vehicleWindowState.getLeftFront())), + channelToBeUpdated); + updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR, + StringType.valueOf(Converter.toTitleCase( + isLeftSteering ? vehicleWindowState.getRightRear() : vehicleWindowState.getLeftRear())), + channelToBeUpdated); + } + + private void updateRoof(VehicleRoofState vehicleRoofState, @Nullable String channelToBeUpdated) { + updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, + StringType.valueOf(Converter.toTitleCase(vehicleRoofState.getRoofState())), channelToBeUpdated); + } + + private void updatePosition(VehicleLocation location, @Nullable String channelToBeUpdated) { + if (location.getCoordinates().getLatitude() < 0) { + updateChannel(CHANNEL_GROUP_LOCATION, GPS, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_LOCATION, HEADING, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, UnDefType.UNDEF, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, UnDefType.UNDEF, channelToBeUpdated); + } else { + PointType vehicleLocation = PointType.valueOf(Double.toString(location.getCoordinates().getLatitude()) + "," + + Double.toString(location.getCoordinates().getLongitude())); + updateChannel(CHANNEL_GROUP_LOCATION, GPS, vehicleLocation, channelToBeUpdated); + updateChannel(CHANNEL_GROUP_LOCATION, HEADING, + QuantityType.valueOf(location.getHeading(), Units.DEGREE_ANGLE), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, StringType.valueOf(location.getAddress().getFormatted()), + channelToBeUpdated); + PointType homeLocation = locationProvider.getLocation(); + if (homeLocation != null) { + updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, + QuantityType.valueOf(vehicleLocation.distanceFrom(homeLocation).intValue(), SIUnits.METRE), + channelToBeUpdated); + } else { + updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, UnDefType.UNDEF, channelToBeUpdated); + } + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.trace("xxxVehicleHandler.handleCommand {}, {}, {}", command.toFullString(), channelUID.getAsString(), + channelUID.getIdWithoutGroup()); + String group = channelUID.getGroupId(); + + if (group == null) { + logger.debug("Cannot handle command {}, no group for channel {}", command.toFullString(), + channelUID.getAsString()); + return; + } + + if (command instanceof RefreshType) { + // Refresh of Channels with cached values + if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) { + imageCache.ifPresent(image -> updateImage(image)); } else { - logger.debug("Content not valid"); + vehicleStatusCache.ifPresent( + vehicleStatus -> triggerVehicleStatusUpdate(vehicleStatus, channelUID.getIdWithoutGroup())); + } + } else if (command instanceof StringType) { + // Check for Channel Group and corresponding Actions + switch (group) { + case CHANNEL_GROUP_REMOTE: + // Executing Remote Services + String serviceCommand = ((StringType) command).toFullString(); + remote.ifPresent(remot -> { + RemoteServiceUtils.getRemoteServiceFromCommand(serviceCommand) + .ifPresentOrElse(service -> remot.execute(service), () -> { + logger.debug("Remote service execution {} unknown", serviceCommand); + }); + }); + break; + case CHANNEL_GROUP_VEHICLE_IMAGE: + // Image Change + vehicleConfiguration.ifPresent(config -> { + if (channelUID.getIdWithoutGroup().equals(IMAGE_VIEWPORT)) { + String newViewport = command.toString(); + synchronized (imageProperties) { + if (!imageProperties.viewport.equals(newViewport)) { + imageProperties = new ImageProperties(newViewport); + imageCache = Optional.empty(); + Optional imageContent = proxy.map(prox -> { + try { + return prox.requestImage(config.getVin(), config.getVehicleBrand(), + imageProperties); + } catch (NetworkException e) { + logger.debug("{}", e.toString()); + return "".getBytes(); + } + }); + imageContent.ifPresent(imageContentData -> updateImage(imageContentData)); + } + } + updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport), + IMAGE_VIEWPORT); + } + }); + break; + case CHANNEL_GROUP_SERVICE: + int serviceIndex = Converter.parseIntegerString(command.toFullString()); + if (serviceIndex != -1) { + selectService(serviceIndex, null); + } else { + logger.debug("Cannot select Service index {}", command.toFullString()); + } + break; + case CHANNEL_GROUP_CHECK_CONTROL: + int checkControlIndex = Converter.parseIntegerString(command.toFullString()); + if (checkControlIndex != -1) { + selectCheckControl(checkControlIndex, null); + } else { + logger.debug("Cannot select CheckControl index {}", command.toFullString()); + } + break; + case CHANNEL_GROUP_CHARGE_SESSION: + int sessionIndex = Converter.parseIntegerString(command.toFullString()); + if (sessionIndex != -1) { + selectSession(sessionIndex, null); + } else { + logger.debug("Cannot select Session index {}", command.toFullString()); + } + break; + default: + logger.debug("Cannot handle command {}, channel {} in group {} not a command channel", + command.toFullString(), channelUID.getAsString(), group); } } + } - @Override - public void onError(NetworkError error) { - logger.debug("{}", error.toString()); + private void updateImage(byte[] imageContent) { + if (imageContent.length > 0) { + imageCache = Optional.of(imageContent); + String contentType = HttpUtil.guessContentTypeFromData(imageContent); + updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_FORMAT, new RawType(imageContent, contentType), + IMAGE_FORMAT); + } else { + synchronized (imageProperties) { + imageProperties.failed(); + } } } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java new file mode 100644 index 0000000000000..bc25fceee2dee --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java @@ -0,0 +1,381 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.auth; + +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.API_OAUTH_CONFIG; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.APP_VERSIONS; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.AUTHORIZATION_CODE; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.AUTH_PROVIDER; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.BRAND_BMW; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.CHINA_LOGIN; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.CHINA_PUBLIC_KEY; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.EADRAX_SERVER_MAP; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.LOGIN_NONCE; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.OAUTH_ENDPOINT; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.OCP_APIM_KEYS; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.REGION_CHINA; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.REGION_NORTH_AMERICA; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.REGION_ROW; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.USER_AGENT; +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.X_USER_AGENT; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.AUTHORIZATION; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CLIENT_ID; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CODE; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CODE_CHALLENGE; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CODE_CHALLENGE_METHOD; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CODE_VERIFIER; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_URL_ENCODED; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.GRANT_TYPE; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_ACP_SUBSCRIPTION_KEY; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_BMW_CORRELATION_ID; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_X_CORRELATION_ID; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_X_IDENTITY_PROVIDER; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_X_USER_AGENT; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.NONCE; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.PASSWORD; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.REDIRECT_URI; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.RESPONSE_TYPE; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.SCOPE; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.STATE; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.USERNAME; + +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.UUID; + +import javax.crypto.Cipher; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpResponseException; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; +import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse; +import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse; +import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse; +import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration; +import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse; +import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer; +import org.openhab.binding.mybmw.internal.utils.Constants; +import org.openhab.binding.mybmw.internal.utils.Converter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * requests the tokens for MyBMW API authorization + * + * thanks to bimmer_connected https://github.com/bimmerconnected/bimmer_connected + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - extracted from myBmwProxy + */ +@NonNullByDefault +public class MyBMWTokenController { + + private final Logger logger = LoggerFactory.getLogger(MyBMWTokenController.class); + + private Token token = new Token(); + private MyBMWBridgeConfiguration configuration; + private HttpClient httpClient; + + public MyBMWTokenController(MyBMWBridgeConfiguration configuration, HttpClient httpClient) { + this.configuration = configuration; + this.httpClient = httpClient; + } + + /** + * Gets new token if old one is expired or invalid. In case of error the token + * remains. + * So if token refresh fails the corresponding requests will also fail and + * update the Thing status accordingly. + * + * @return token + */ + public Token getToken() { + if (!token.isValid()) { + boolean tokenUpdateSuccess = false; + switch (configuration.region) { + case REGION_CHINA: + tokenUpdateSuccess = updateTokenChina(); + break; + case REGION_NORTH_AMERICA: + tokenUpdateSuccess = updateToken(); + break; + case REGION_ROW: + tokenUpdateSuccess = updateToken(); + break; + default: + logger.warn("Region {} not supported", configuration.region); + break; + } + if (!tokenUpdateSuccess) { + logger.warn("Authorization failed!"); + } + } + return token; + } + + /** + * Everything is catched by surroundig try catch + * - HTTP Exceptions + * - JSONSyntax Exceptions + * - potential NullPointer Exceptions + * + * @return + */ + private synchronized boolean updateToken() { + try { + /* + * Step 1) Get basic values for further queries + */ + String uuidString = UUID.randomUUID().toString(); + + String authValuesUrl = "https://" + EADRAX_SERVER_MAP.get(configuration.region) + API_OAUTH_CONFIG; + Request authValuesRequest = httpClient.newRequest(authValuesUrl); + authValuesRequest.header(HEADER_ACP_SUBSCRIPTION_KEY, OCP_APIM_KEYS.get(configuration.region)); + authValuesRequest.header(HEADER_X_USER_AGENT, String.format(X_USER_AGENT, BRAND_BMW, + APP_VERSIONS.get(configuration.region), configuration.region)); + authValuesRequest.header(HEADER_X_IDENTITY_PROVIDER, AUTH_PROVIDER); + authValuesRequest.header(HEADER_X_CORRELATION_ID, uuidString); + authValuesRequest.header(HEADER_BMW_CORRELATION_ID, uuidString); + + ContentResponse authValuesResponse = authValuesRequest.send(); + if (authValuesResponse.getStatus() != 200) { + throw new HttpResponseException("URL: " + authValuesRequest.getURI() + ", Error: " + + authValuesResponse.getStatus() + ", Message: " + authValuesResponse.getContentAsString(), + authValuesResponse); + } + AuthQueryResponse aqr = JsonStringDeserializer.deserializeString(authValuesResponse.getContentAsString(), + AuthQueryResponse.class); + + logger.trace("authQueryResponse: {}", aqr); + + /* + * Step 2) Calculate values for oauth base parameters + */ + String codeVerifier = generateCodeVerifier(); + String codeChallenge = generateCodeChallenge(codeVerifier); + String state = generateState(); + + MultiMap<@Nullable String> baseParams = new MultiMap<>(); + baseParams.put(CLIENT_ID, aqr.clientId); + baseParams.put(RESPONSE_TYPE, CODE); + baseParams.put(REDIRECT_URI, aqr.returnUrl); + baseParams.put(STATE, state); + baseParams.put(NONCE, LOGIN_NONCE); + baseParams.put(SCOPE, String.join(Constants.SPACE, aqr.scopes)); + baseParams.put(CODE_CHALLENGE, codeChallenge); + baseParams.put(CODE_CHALLENGE_METHOD, "S256"); + + /** + * Step 3) Authorization with username and password + */ + String loginUrl = aqr.gcdmBaseUrl + OAUTH_ENDPOINT; + Request loginRequest = httpClient.POST(loginUrl); + + loginRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + + MultiMap<@Nullable String> loginParams = new MultiMap<>(baseParams); + loginParams.put(GRANT_TYPE, AUTHORIZATION_CODE); + loginParams.put(USERNAME, configuration.userName); + loginParams.put(PASSWORD, configuration.password); + loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, + UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); + ContentResponse loginResponse = loginRequest.send(); + if (loginResponse.getStatus() != 200) { + throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: " + + loginResponse.getStatus() + ", Message: " + loginResponse.getContentAsString(), + loginResponse); + } + String authCode = getAuthCode(loginResponse.getContentAsString()); + + /** + * Step 4) Authorize with code + */ + Request authRequest = httpClient.POST(loginUrl).followRedirects(false); + MultiMap<@Nullable String> authParams = new MultiMap<>(baseParams); + authParams.put(AUTHORIZATION, authCode); + authRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, + UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); + ContentResponse authResponse = authRequest.send(); + if (authResponse.getStatus() != 302) { + throw new HttpResponseException("URL: " + authRequest.getURI() + ", Error: " + authResponse.getStatus() + + ", Message: " + authResponse.getContentAsString(), authResponse); + } + String code = codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION)); + + /** + * Step 5) Request token + */ + Request codeRequest = httpClient.POST(aqr.tokenEndpoint); + String basicAuth = "Basic " + + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes()); + codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + codeRequest.header(AUTHORIZATION, basicAuth); + + MultiMap<@Nullable String> codeParams = new MultiMap<>(); + codeParams.put(CODE, code); + codeParams.put(CODE_VERIFIER, codeVerifier); + codeParams.put(REDIRECT_URI, aqr.returnUrl); + codeParams.put(GRANT_TYPE, AUTHORIZATION_CODE); + codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, + UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); + ContentResponse codeResponse = codeRequest.send(); + if (codeResponse.getStatus() != 200) { + throw new HttpResponseException("URL: " + codeRequest.getURI() + ", Error: " + codeResponse.getStatus() + + ", Message: " + codeResponse.getContentAsString(), codeResponse); + } + AuthResponse ar = JsonStringDeserializer.deserializeString(codeResponse.getContentAsString(), + AuthResponse.class); + token.setType(ar.tokenType); + token.setToken(ar.accessToken); + token.setExpiration(ar.expiresIn); + return true; + } catch (Exception e) { + logger.warn("Authorization Exception: {}", e.getMessage()); + } + return false; + } + + private String generateState() { + String stateBytes = Converter.getRandomString(16); + return Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes()); + } + + private String generateCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hash); + } + + private String generateCodeVerifier() { + String verfifierBytes = Converter.getRandomString(64); + return Base64.getUrlEncoder().withoutPadding().encodeToString(verfifierBytes.getBytes()); + } + + private String getAuthCode(String response) { + String[] keys = response.split("&"); + for (int i = 0; i < keys.length; i++) { + if (keys[i].startsWith(AUTHORIZATION)) { + String authCode = keys[i].split("=")[1]; + authCode = authCode.split("\"")[0]; + return authCode; + } + } + return Constants.EMPTY; + } + + private String codeFromUrl(String encodedUrl) { + final MultiMap<@Nullable String> tokenMap = new MultiMap<>(); + UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII); + final StringBuilder codeFound = new StringBuilder(); + tokenMap.forEach((key, value) -> { + if (value.size() > 0) { + String val = value.get(0); + if (key.endsWith(CODE) && (val != null)) { + codeFound.append(val.toString()); + } + } + }); + return codeFound.toString(); + } + + private synchronized boolean updateTokenChina() { + try { + /** + * Step 1) get public key + */ + String publicKeyUrl = "https://" + EADRAX_SERVER_MAP.get(REGION_CHINA) + CHINA_PUBLIC_KEY; + Request oauthQueryRequest = httpClient.newRequest(publicKeyUrl); + oauthQueryRequest.header(HttpHeader.USER_AGENT, USER_AGENT); + oauthQueryRequest.header(HEADER_X_USER_AGENT, String.format(X_USER_AGENT, BRAND_BMW, + APP_VERSIONS.get(configuration.region), configuration.region)); + ContentResponse publicKeyResponse = oauthQueryRequest.send(); + if (publicKeyResponse.getStatus() != 200) { + throw new HttpResponseException("URL: " + oauthQueryRequest.getURI() + ", Error: " + + publicKeyResponse.getStatus() + ", Message: " + publicKeyResponse.getContentAsString(), + publicKeyResponse); + } + ChinaPublicKeyResponse pkr = JsonStringDeserializer + .deserializeString(publicKeyResponse.getContentAsString(), ChinaPublicKeyResponse.class); + + /** + * Step 2) Encode password with public key + */ + // https://www.baeldung.com/java-read-pem-file-keys + String publicKeyStr = pkr.data.value; + String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "") + .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "") + .replace("\\n", "").trim(); + byte[] encoded = Base64.getDecoder().decode(publicKeyPEM); + X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded); + KeyFactory kf = KeyFactory.getInstance("RSA"); + PublicKey publicKey = kf.generatePublic(spec); + // https://www.thexcoders.net/java-ciphers-rsa/ + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] encryptedBytes = cipher.doFinal(configuration.password.getBytes()); + String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes); + + /** + * Step 3) Send Auth with encoded password + */ + String tokenUrl = "https://" + EADRAX_SERVER_MAP.get(REGION_CHINA) + CHINA_LOGIN; + Request loginRequest = httpClient.POST(tokenUrl); + loginRequest.header(HEADER_X_USER_AGENT, String.format(X_USER_AGENT, BRAND_BMW, + APP_VERSIONS.get(configuration.region), configuration.region)); + String jsonContent = "{ \"mobile\":\"" + configuration.userName + "\", \"password\":\"" + encodedPassword + + "\"}"; + loginRequest.content(new StringContentProvider(jsonContent)); + ContentResponse tokenResponse = loginRequest.send(); + if (tokenResponse.getStatus() != 200) { + throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: " + + tokenResponse.getStatus() + ", Message: " + tokenResponse.getContentAsString(), + tokenResponse); + } + String authCode = getAuthCode(tokenResponse.getContentAsString()); + + /** + * Step 4) Decode access token + */ + ChinaTokenResponse cat = JsonStringDeserializer.deserializeString(authCode, ChinaTokenResponse.class); + String token = cat.data.accessToken; + // https://www.baeldung.com/java-jwt-token-decode + String[] chunks = token.split("\\."); + String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1])); + ChinaTokenExpiration cte = JsonStringDeserializer.deserializeString(tokenJwtDecodeStr, + ChinaTokenExpiration.class); + Token t = new Token(); + t.setToken(token); + t.setType(cat.data.tokenType); + t.setExpirationTotal(cte.exp); + return true; + } catch (Exception e) { + logger.warn("Authorization Exception: {}", e.getMessage()); + } + return false; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/Token.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/Token.java similarity index 96% rename from bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/Token.java rename to bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/Token.java index 7f286ec055768..45bd6adac584f 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/Token.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/Token.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mybmw.internal.handler; +package org.openhab.binding.mybmw.internal.handler.auth; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.mybmw.internal.utils.Constants; diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java new file mode 100644 index 0000000000000..74b439fc6d3d6 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; +import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * + * deserialization of a JSON string to a Java Object + * + * @author Martin Grassl - initial contribution + */ +@NonNullByDefault +public interface JsonStringDeserializer { + + static final Logger LOGGER = LoggerFactory.getLogger(JsonStringDeserializer.class); + + static final Gson GSON = new Gson(); + + public static List getVehicleBaseList(String vehicleBaseJson) { + try { + VehicleBase[] vehicleBaseArray = deserializeString(vehicleBaseJson, VehicleBase[].class); + return Arrays.asList(vehicleBaseArray); + } catch (JsonSyntaxException e) { + LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + return new ArrayList(); + } + } + + public static VehicleStateContainer getVehicleState(String vehicleStateJson) { + try { + VehicleStateContainer vehicleState = deserializeString(vehicleStateJson, VehicleStateContainer.class); + vehicleState.setRawStateJson(vehicleStateJson); + return vehicleState; + } catch (JsonSyntaxException e) { + LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + return new VehicleStateContainer(); + } + } + + public static ChargingStatisticsContainer getChargingStatistics(String chargeStatisticsJson) { + try { + ChargingStatisticsContainer chargeStatistics = deserializeString(chargeStatisticsJson, + ChargingStatisticsContainer.class); + return chargeStatistics; + } catch (JsonSyntaxException e) { + LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + return new ChargingStatisticsContainer(); + } + } + + public static ChargingSessionsContainer getChargingSessions(String chargeSessionsJson) { + try { + return deserializeString(chargeSessionsJson, ChargingSessionsContainer.class); + } catch (JsonSyntaxException e) { + LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + return new ChargingSessionsContainer(); + } + } + + public static ExecutionStatusContainer getExecutionStatus(String executionStatusJson) { + try { + return deserializeString(executionStatusJson, ExecutionStatusContainer.class); + } catch (JsonSyntaxException e) { + LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + return new ExecutionStatusContainer(); + } + } + + public static T deserializeString(String toBeDeserialized, Class deserializedClass) { + return GSON.fromJson(toBeDeserialized, deserializedClass); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java new file mode 100644 index 0000000000000..1b44f62bf298e --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; +import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; +import org.openhab.binding.mybmw.internal.utils.BimmerConstants; +import org.openhab.binding.mybmw.internal.utils.ImageProperties; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is for local testing. You have to configure a connected account with username = "testuser" and password = + * vehicle to be tested (e.g. BEV, ICE, BEV2, MILD_HYBRID,...) + * The respective files are loaded from the resources folder + * + * You have to set the environment variable "ENVIRONMENT" to the value "test" + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactoring + */ +@NonNullByDefault +public class MyBMWFileProxy implements MyBMWProxy { + private final Logger logger = LoggerFactory.getLogger(MyBMWFileProxy.class); + private String vehicleToBeTested; + + private static final String RESPONSES = "responses" + File.separator; + private static final String VEHICLES_BASE = File.separator + "vehicles_base.json"; + private static final String VEHICLES_STATE = File.separator + "vehicles_state.json"; + private static final String CHARGING_SESSIONS = File.separator + "charging_sessions.json"; + private static final String CHARGING_STATISTICS = File.separator + "charging_statistics.json"; + private static final String REMOTE_SERVICES_CALL = File.separator + "remote_service_call.json"; + private static final String REMOTE_SERVICES_STATE = File.separator + "remote_service_status.json"; + + public MyBMWFileProxy(HttpClientFactory httpClientFactory, MyBMWBridgeConfiguration bridgeConfiguration) { + logger.trace("MyBMWFileProxy - initialize"); + vehicleToBeTested = bridgeConfiguration.password; + } + + public void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration) { + logger.trace("MyBMWFileProxy - update bridge"); + vehicleToBeTested = bridgeConfiguration.password; + } + + public List<@NonNull Vehicle> requestVehicles() throws NetworkException { + List<@NonNull Vehicle> vehicles = new ArrayList<>(); + List<@NonNull VehicleBase> vehiclesBase = requestVehiclesBase(); + + for (VehicleBase vehicleBase : vehiclesBase) { + VehicleStateContainer vehicleState = requestVehicleState(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand()); + + Vehicle vehicle = new Vehicle(); + vehicle.setVehicleBase(vehicleBase); + vehicle.setVehicleState(vehicleState); + vehicles.add(vehicle); + } + + return vehicles; + } + + /** + * request all vehicles for one specific brand and their state + * + * @param brand + */ + public List requestVehiclesBase(String brand) throws NetworkException { + String vehicleResponseString = requestVehiclesBaseJson(brand); + return JsonStringDeserializer.getVehicleBaseList(vehicleResponseString); + } + + public String requestVehiclesBaseJson(String brand) throws NetworkException { + String vehicleResponseString = fileToString(VEHICLES_BASE); + return vehicleResponseString; + } + + /** + * request vehicles for all possible brands + * + * @param callback + */ + public List requestVehiclesBase() throws NetworkException { + List vehicles = new ArrayList<>(); + + for (String brand : BimmerConstants.REQUESTED_BRANDS) { + vehicles.addAll(requestVehiclesBase(brand)); + } + + return vehicles; + } + + /** + * request the vehicle image + * + * @param config + * @param props + * @return + */ + public byte[] requestImage(String vin, String brand, ImageProperties props) throws NetworkException { + return "".getBytes(); + } + + /** + * request the state for one specific vehicle + * + * @param baseVehicle + * @return + */ + public VehicleStateContainer requestVehicleState(String vin, String brand) throws NetworkException { + String vehicleStateResponseString = requestVehicleStateJson(vin, brand); + return JsonStringDeserializer.getVehicleState(vehicleStateResponseString); + } + + public String requestVehicleStateJson(String vin, String brand) throws NetworkException { + String vehicleStateResponseString = fileToString(VEHICLES_STATE); + return vehicleStateResponseString; + } + + /** + * request charge statistics for electric vehicles + * + */ + public ChargingStatisticsContainer requestChargeStatistics(String vin, String brand) throws NetworkException { + String chargeStatisticsResponseString = requestChargeStatisticsJson(vin, brand); + return JsonStringDeserializer.getChargingStatistics(new String(chargeStatisticsResponseString)); + } + + public String requestChargeStatisticsJson(String vin, String brand) throws NetworkException { + String chargeStatisticsResponseString = fileToString(CHARGING_STATISTICS); + return chargeStatisticsResponseString; + } + + /** + * request charge sessions for electric vehicles + * + */ + public ChargingSessionsContainer requestChargeSessions(String vin, String brand) throws NetworkException { + String chargeSessionsResponseString = requestChargeSessionsJson(vin, brand); + return JsonStringDeserializer.getChargingSessions(chargeSessionsResponseString); + } + + public String requestChargeSessionsJson(String vin, String brand) throws NetworkException { + String chargeSessionsResponseString = fileToString(CHARGING_SESSIONS); + return chargeSessionsResponseString; + } + + public ExecutionStatusContainer executeRemoteServiceCall(String vin, String brand, RemoteService service) + throws NetworkException { + return JsonStringDeserializer.getExecutionStatus(fileToString(REMOTE_SERVICES_CALL)); + } + + public ExecutionStatusContainer executeRemoteServiceStatusCall(String brand, String eventId) + throws NetworkException { + return JsonStringDeserializer.getExecutionStatus(fileToString(REMOTE_SERVICES_STATE)); + } + + private String fileToString(String filename) { + logger.trace("reading file {}", RESPONSES + vehicleToBeTested + filename); + try (BufferedReader br = new BufferedReader(new InputStreamReader( + MyBMWFileProxy.class.getClassLoader().getResourceAsStream(RESPONSES + vehicleToBeTested + filename), "UTF-8"))) { + StringBuilder buf = new StringBuilder(); + String sCurrentLine; + + while ((sCurrentLine = br.readLine()) != null) { + buf.append(sCurrentLine); + } + logger.trace("successful"); + return buf.toString(); + } catch (IOException e) { + logger.error("file {} could not be loaded: {}", filename, e.getMessage()); + return ""; + } + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java new file mode 100644 index 0000000000000..763f392559d02 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -0,0 +1,346 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import static org.openhab.binding.mybmw.internal.utils.BimmerConstants.APP_VERSIONS; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; +import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.handler.auth.MyBMWTokenController; +import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; +import org.openhab.binding.mybmw.internal.utils.BimmerConstants; +import org.openhab.binding.mybmw.internal.utils.Constants; +import org.openhab.binding.mybmw.internal.utils.Converter; +import org.openhab.binding.mybmw.internal.utils.HTTPConstants; +import org.openhab.binding.mybmw.internal.utils.ImageProperties; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link MyBMWHttpProxy} 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 + * @author Martin Grassl - refactoring + * @author Mark Herwege - extended log anonymization + */ +@NonNullByDefault +public class MyBMWHttpProxy implements MyBMWProxy { + private final Logger logger = LoggerFactory.getLogger(MyBMWHttpProxy.class); + private final HttpClient httpClient; + private MyBMWBridgeConfiguration bridgeConfiguration; + private final MyBMWTokenController myBMWTokenHandler; + + /** + * URLs taken from + * https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py + */ + private final String vehicleUrl; + private final String vehicleStateUrl; + private final String remoteCommandUrl; + private final String remoteStatusUrl; + + public MyBMWHttpProxy(HttpClientFactory httpClientFactory, MyBMWBridgeConfiguration bridgeConfiguration) { + logger.trace("MyBMWHttpProxy - initialize"); + httpClient = httpClientFactory.getCommonHttpClient(); + + myBMWTokenHandler = new MyBMWTokenController(bridgeConfiguration, httpClient); + + this.bridgeConfiguration = bridgeConfiguration; + + vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + + BimmerConstants.API_VEHICLES; + + vehicleStateUrl = vehicleUrl + "/state"; + + remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + + BimmerConstants.API_REMOTE_SERVICE_BASE_URL; + remoteStatusUrl = remoteCommandUrl + "eventStatus"; + } + + @Override + public void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration) { + this.bridgeConfiguration = bridgeConfiguration; + } + + public List<@NonNull Vehicle> requestVehicles() throws NetworkException { + List<@NonNull Vehicle> vehicles = new ArrayList<>(); + List<@NonNull VehicleBase> vehiclesBase = requestVehiclesBase(); + + for (VehicleBase vehicleBase : vehiclesBase) { + VehicleStateContainer vehicleState = requestVehicleState(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand()); + + Vehicle vehicle = new Vehicle(); + vehicle.setVehicleBase(vehicleBase); + vehicle.setVehicleState(vehicleState); + vehicles.add(vehicle); + } + + return vehicles; + } + + /** + * request all vehicles for one specific brand and their state + * + * @param brand + */ + public List requestVehiclesBase(String brand) throws NetworkException { + String vehicleResponseString = requestVehiclesBaseJson(brand); + return JsonStringDeserializer.getVehicleBaseList(vehicleResponseString); + } + + public String requestVehiclesBaseJson(String brand) throws NetworkException { + byte[] vehicleResponse = get(vehicleUrl, brand, null, HTTPConstants.CONTENT_TYPE_JSON); + String vehicleResponseString = new String(vehicleResponse, Charset.defaultCharset()); + return vehicleResponseString; + } + + /** + * request vehicles for all possible brands + * + * @param callback + */ + public List requestVehiclesBase() throws NetworkException { + List vehicles = new ArrayList<>(); + + for (String brand : BimmerConstants.REQUESTED_BRANDS) { + vehicles.addAll(requestVehiclesBase(brand)); + } + + return vehicles; + } + + /** + * request the vehicle image + * + * @param config + * @param props + * @return + */ + public byte[] requestImage(String vin, String brand, ImageProperties props) throws NetworkException { + final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + + "/eadrax-ics/v3/presentation/vehicles/" + vin + "/images?carView=" + props.viewport; + return get(localImageUrl, brand, vin, HTTPConstants.CONTENT_TYPE_IMAGE); + } + + /** + * request the state for one specific vehicle + * + * @param baseVehicle + * @return + */ + public VehicleStateContainer requestVehicleState(String vin, String brand) throws NetworkException { + String vehicleStateResponseString = requestVehicleStateJson(vin, brand); + return JsonStringDeserializer.getVehicleState(vehicleStateResponseString); + } + + public String requestVehicleStateJson(String vin, String brand) throws NetworkException { + byte[] vehicleStateResponse = get(vehicleStateUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON); + String vehicleStateResponseString = new String(vehicleStateResponse, Charset.defaultCharset()); + return vehicleStateResponseString; + } + + /** + * request charge statistics for electric vehicles + * + */ + public ChargingStatisticsContainer requestChargeStatistics(String vin, String brand) throws NetworkException { + String chargeStatisticsResponseString = requestChargeStatisticsJson(vin, brand); + return JsonStringDeserializer.getChargingStatistics(new String(chargeStatisticsResponseString)); + } + + public String requestChargeStatisticsJson(String vin, String brand) throws NetworkException { + MultiMap<@Nullable String> chargeStatisticsParams = new MultiMap<>(); + chargeStatisticsParams.put("vin", vin); + chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime()); + String params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false); + String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + + "/eadrax-chs/v1/charging-statistics?" + params; + byte[] chargeStatisticsResponse = get(chargeStatisticsUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON); + String chargeStatisticsResponseString = new String(chargeStatisticsResponse); + return chargeStatisticsResponseString; + } + + /** + * request charge sessions for electric vehicles + * + */ + public ChargingSessionsContainer requestChargeSessions(String vin, String brand) throws NetworkException { + String chargeSessionsResponseString = requestChargeSessionsJson(vin, brand); + return JsonStringDeserializer.getChargingSessions(chargeSessionsResponseString); + } + + public String requestChargeSessionsJson(String vin, String brand) throws NetworkException { + MultiMap<@Nullable String> chargeSessionsParams = new MultiMap<>(); + chargeSessionsParams.put("vin", vin); + chargeSessionsParams.put("maxResults", "40"); + chargeSessionsParams.put("include_date_picker", "true"); + String params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false); + String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + + "/eadrax-chs/v1/charging-sessions?" + params; + byte[] chargeSessionsResponse = get(chargeSessionsUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON); + String chargeSessionsResponseString = new String(chargeSessionsResponse); + return chargeSessionsResponseString; + } + + public ExecutionStatusContainer executeRemoteServiceCall(String vin, String brand, RemoteService service) + throws NetworkException { + String executionUrl = remoteCommandUrl + vin + "/" + service.getCommand(); + + byte[] response = post(executionUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON, service.getBody()); + + return JsonStringDeserializer.getExecutionStatus(new String(response)); + } + + public ExecutionStatusContainer executeRemoteServiceStatusCall(String brand, String eventId) + throws NetworkException { + String executionUrl = remoteStatusUrl + Constants.QUESTION + "eventId=" + eventId; + + byte[] response = post(executionUrl, brand, null, HTTPConstants.CONTENT_TYPE_JSON, null); + + return JsonStringDeserializer.getExecutionStatus(new String(response)); + } + + /** + * prepares a GET request to the backend + * + * @param url + * @param coding + * @param params + * @param brand + * @param contentType + * @return + */ + private byte[] get(String url, final String brand, @Nullable String vin, String contentType) + throws NetworkException { + return call(url, false, brand, vin, contentType, null); + } + + /** + * prepares a POST request to the backend + * + * @param url + * @param coding + * @param params + * @param brand + * @param contentType + * @return + */ + private byte[] post(String url, final String brand, @Nullable String vin, String contentType, @Nullable String body) + throws NetworkException { + return call(url, true, brand, vin, contentType, body); + } + + /** + * executes the real call to the backend + * + * @param url + * @param post + * @param encoding + * @param queryParams + * @param brand + * @param contentType + * @return + */ + private synchronized byte[] call(final String url, final boolean post, final String brand, + final @Nullable String vin, final String contentType, final @Nullable String body) throws NetworkException { + byte[] responseByteArray = "".getBytes(); + + // return in case of unknown brand + if (!BimmerConstants.REQUESTED_BRANDS.contains(brand.toLowerCase())) { + logger.warn("Unknown Brand {}", brand); + throw new NetworkException("Unknown Brand " + brand); + } + + final Request req; + + if (post) { + req = httpClient.POST(url); + } else { + req = httpClient.newRequest(url); + } + + req.header(HttpHeader.AUTHORIZATION, myBMWTokenHandler.getToken().getBearerToken()); + req.header(HTTPConstants.HEADER_X_USER_AGENT, String.format(BimmerConstants.X_USER_AGENT, brand, + APP_VERSIONS.get(bridgeConfiguration.region), bridgeConfiguration.region)); + req.header(HttpHeader.ACCEPT_LANGUAGE, bridgeConfiguration.language); + req.header(HttpHeader.ACCEPT, contentType); + req.header(HTTPConstants.HEADER_BMW_VIN, vin); + + try { + ContentResponse response = req.timeout(HTTPConstants.HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); + if (response.getStatus() >= 300) { + responseByteArray = "".getBytes(); + NetworkException exception = new NetworkException(url, response.getStatus(), + ResponseContentAnonymizer.anonymizeResponseContent(response.getContentAsString()), body); + logResponse(ResponseContentAnonymizer.replaceVin(exception.getUrl(), vin), exception.getReason(), + ResponseContentAnonymizer.anonymizeResponseContent(body)); + throw exception; + } else { + responseByteArray = response.getContent(); + + // don't print images + if (!HTTPConstants.CONTENT_TYPE_IMAGE.equals(contentType)) { + logResponse(ResponseContentAnonymizer.replaceVin(url, vin), + ResponseContentAnonymizer.anonymizeResponseContent(response.getContentAsString()), + ResponseContentAnonymizer.anonymizeResponseContent(body)); + } + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logResponse(ResponseContentAnonymizer.replaceVin(url, vin), e.getMessage(), + ResponseContentAnonymizer.anonymizeResponseContent(vin)); + throw new NetworkException(url, -1, null, body, e); + } + + return responseByteArray; + } + + private void logResponse(@Nullable String url, @Nullable String fingerprint, @Nullable String body) { + logger.debug("###### Request URL - BEGIN ######"); + logger.debug("{}", url); + logger.debug("###### Request Body - BEGIN ######"); + logger.debug("{}", body); + logger.debug("###### Response Data - BEGIN ######"); + logger.debug("{}", fingerprint); + logger.debug("###### Response Data - END ######"); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java new file mode 100644 index 0000000000000..0d3e29db3f402 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; +import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; +import org.openhab.binding.mybmw.internal.utils.ImageProperties; + +/** + * this is the interface for requesting the myBMW responses + * + * @author Martin Grassl - Initial Contribution + */ +@NonNullByDefault +public interface MyBMWProxy { + + void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration); + + List<@NonNull Vehicle> requestVehicles() throws NetworkException; + + /** + * request all vehicles for one specific brand and their state + * + * @param brand + */ + List requestVehiclesBase(String brand) throws NetworkException; + + String requestVehiclesBaseJson(String brand) throws NetworkException; + + /** + * request vehicles for all possible brands + * + * @param callback + */ + List requestVehiclesBase() throws NetworkException; + + /** + * request the vehicle image + * + * @param config + * @param props + * @return + */ + byte[] requestImage(String vin, String brand, ImageProperties props) throws NetworkException; + + /** + * request the state for one specific vehicle + * + * @param baseVehicle + * @return + */ + VehicleStateContainer requestVehicleState(String vin, String brand) throws NetworkException; + + String requestVehicleStateJson(String vin, String brand) throws NetworkException; + + /** + * request charge statistics for electric vehicles + * + */ + ChargingStatisticsContainer requestChargeStatistics(String vin, String brand) throws NetworkException; + + String requestChargeStatisticsJson(String vin, String brand) throws NetworkException; + + /** + * request charge sessions for electric vehicles + * + */ + ChargingSessionsContainer requestChargeSessions(String vin, String brand) throws NetworkException; + + String requestChargeSessionsJson(String vin, String brand) throws NetworkException; + + ExecutionStatusContainer executeRemoteServiceCall(String vin, String brand, RemoteService service) + throws NetworkException; + + ExecutionStatusContainer executeRemoteServiceStatusCall(String brand, String eventId) throws NetworkException; +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/NetworkException.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/NetworkException.java new file mode 100644 index 0000000000000..5397733400152 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/NetworkException.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link NetworkException} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - extend Exception + */ +@NonNullByDefault +public class NetworkException extends Exception { + + private static final long serialVersionUID = 123L; + + private String url = ""; + private int status = -1; + private String reason = ""; + private String body = ""; + + public NetworkException() { + } + + public NetworkException(String url, int status, @Nullable String reason, @Nullable String body) { + this.url = url; + this.status = status; + this.reason = reason != null ? reason : ""; + this.body = body != null ? body : ""; + } + + public NetworkException(String url, int status, @Nullable String reason, @Nullable String body, Throwable cause) { + super(cause); + this.url = url; + this.status = status; + this.reason = reason != null ? reason : ""; + this.body = body != null ? body : ""; + } + + public NetworkException(String message) { + super(message); + this.reason = message; + } + + public NetworkException(Throwable cause) { + super(cause); + } + + public NetworkException(String message, Throwable cause) { + super(message, cause); + this.reason = message; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + @Override + public String toString() { + return "NetworkException [url=" + url + ", status=" + status + ", reason=" + reason + ", body=" + body + "]"; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizer.java new file mode 100644 index 0000000000000..f3107abf8ab17 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizer.java @@ -0,0 +1,245 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * + * anonymizes all occurrencies of locations and vins + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactoring & extension for any occurrence + * @author Mark Herwege - extended log anonymization + */ +@NonNullByDefault +public interface ResponseContentAnonymizer { + + static final String ANONYMOUS_VIN = "anonymousVin"; + static final String VIN_PATTERN = "\"vin\":"; + static final String VEHICLE_CHARGING_LOCATION_PATTERN = "\"subtitle\":"; + static final String VEHICLE_LOCATION_PATTERN = "\"location\":"; + static final String VEHICLE_LOCATION_LATITUDE_PATTERN = "latitude"; + static final String VEHICLE_LOCATION_LONGITUDE_PATTERN = "longitude"; + static final String VEHICLE_LOCATION_FORMATTED_PATTERN = "formatted"; + static final String VEHICLE_LOCATION_HEADING_PATTERN = "heading"; + static final String VEHICLE_LOCATION_LATITUDE = "1.1"; + static final String VEHICLE_LOCATION_LONGITUDE = "2.2"; + static final String ANONYMOUS_ADDRESS = "anonymousAddress"; + static final String VEHICLE_LOCATION_HEADING = "-1"; + static final String RAW_VEHICLE_LOCATION_PATTERN_START = "\\\"location\\\""; + static final String RAW_VEHICLE_LOCATION_PATTERN_END = "\\\"heading\\\""; + static final String RAW_VEHICLE_LOCATION_PATTERN_REPLACER = "\"location\":{\"coordinates\":{\"latitude\":" + + VEHICLE_LOCATION_LATITUDE + ",\"longitude\":" + VEHICLE_LOCATION_LONGITUDE + + "},\"address\":{\"formatted\":\"" + ANONYMOUS_ADDRESS + "\"},"; + + static final String CLOSING_BRACKET = "}"; + static final String QUOTE = "\""; + static final String CLOSE_VALUE = "\":"; + static final String COMMA = ","; + + /** + * anonymizes the responseContent + *

+ * - vin + *

+ *

+ * - location + *

+ * + * @param responseContent + * @return + */ + public static String anonymizeResponseContent(@Nullable String responseContent) { + if (responseContent == null) { + return ""; + } + + String anonymizedVinString = replaceVins(responseContent); + + String anonymizedLocationString = replaceLocations(anonymizedVinString); + + String anonymizedRawLocationString = replaceRawLocations(anonymizedLocationString); + + String anonymizedChargingLocationString = replaceChargingLocations(anonymizedRawLocationString); + + return anonymizedChargingLocationString; + } + + static String replaceChargingLocations(String stringToBeReplaced) { + String[] locationStrings = stringToBeReplaced.split(VEHICLE_CHARGING_LOCATION_PATTERN); + + StringBuffer replacedString = new StringBuffer(); + replacedString.append(locationStrings[0]); + for (int i = 1; locationStrings.length > 0 && i < locationStrings.length && locationStrings[i] != null; i++) { + replacedString.append(VEHICLE_CHARGING_LOCATION_PATTERN); + replacedString.append(replaceChargingLocation(locationStrings[i])); + } + + return replacedString.toString(); + } + + static String replaceChargingLocation(String responseContent) { + String[] subtitleStrings = responseContent.split(" • ", 2); + + StringBuffer replacedString = new StringBuffer(); + + replacedString.append("\""); + replacedString.append(ANONYMOUS_ADDRESS); + if (subtitleStrings.length > 1) { + replacedString.append(" • "); + replacedString.append(subtitleStrings[1]); + } + + return replacedString.toString(); + } + + static String replaceRawLocations(String stringToBeReplaced) { + String[] locationStrings = stringToBeReplaced.split(Pattern.quote(RAW_VEHICLE_LOCATION_PATTERN_START)); + + StringBuffer replacedString = new StringBuffer(); + replacedString.append(locationStrings[0]); + for (int i = 1; locationStrings.length > 0 && i < locationStrings.length && locationStrings[i] != null; i++) { + replacedString.append(replaceRawLocation(locationStrings[i])); + } + + return replacedString.toString(); + } + + /** + * this just replaces a string + * + * @param string + * @return + */ + static String replaceRawLocation(String stringToBeReplaced) { + String[] stringParts = stringToBeReplaced.split(Pattern.quote(RAW_VEHICLE_LOCATION_PATTERN_END)); + + StringBuffer replacedString = new StringBuffer(); + replacedString.append(RAW_VEHICLE_LOCATION_PATTERN_REPLACER); + replacedString.append(RAW_VEHICLE_LOCATION_PATTERN_END); + replacedString.append(stringParts[1]); + return replacedString.toString(); + } + + static String replaceLocations(String stringToBeReplaced) { + String[] locationStrings = stringToBeReplaced.split(VEHICLE_LOCATION_PATTERN); + + StringBuffer replacedString = new StringBuffer(); + replacedString.append(locationStrings[0]); + for (int i = 1; locationStrings.length > 0 && i < locationStrings.length && locationStrings[i] != null; i++) { + replacedString.append(VEHICLE_LOCATION_PATTERN); + replacedString.append(replaceLocation(locationStrings[i])); + } + + return replacedString.toString(); + } + + static String replaceLocation(String responseContent) { + String stringToBeReplaced = responseContent; + + StringBuffer replacedString = new StringBuffer(); + // latitude + stringToBeReplaced = replaceNumberValue(stringToBeReplaced, replacedString, VEHICLE_LOCATION_LATITUDE_PATTERN, + VEHICLE_LOCATION_LATITUDE); + + // longitude + stringToBeReplaced = replaceNumberValue(stringToBeReplaced, replacedString, VEHICLE_LOCATION_LONGITUDE_PATTERN, + VEHICLE_LOCATION_LONGITUDE); + + // formatted address + stringToBeReplaced = replaceStringValue(stringToBeReplaced, replacedString, VEHICLE_LOCATION_FORMATTED_PATTERN, + ANONYMOUS_ADDRESS); + + // heading + stringToBeReplaced = replaceNumberValue(stringToBeReplaced, replacedString, VEHICLE_LOCATION_HEADING_PATTERN, + VEHICLE_LOCATION_HEADING); + + replacedString.append(stringToBeReplaced); + + return replacedString.toString(); + } + + static String replaceNumberValue(String stringToBeReplaced, StringBuffer replacedString, String replacerPattern, + String replacerValue) { + int startIndex = stringToBeReplaced.indexOf(replacerPattern, 1) + + (replacerPattern.length() + CLOSE_VALUE.length()); + int endIndex = -1; + + // in an object, the comma comes after the value or a closing bracket + if (stringToBeReplaced.indexOf(COMMA, startIndex) < stringToBeReplaced.indexOf(CLOSING_BRACKET, startIndex)) { + endIndex = stringToBeReplaced.indexOf(COMMA, startIndex); + } else { + endIndex = stringToBeReplaced.indexOf(CLOSING_BRACKET, startIndex); + } + + replacedString.append(stringToBeReplaced.substring(0, startIndex)); + replacedString.append(replacerValue); + + return stringToBeReplaced.substring(endIndex); + } + + static String replaceStringValue(String stringToBeReplaced, StringBuffer replacedString, String replacerPattern, + String replacerValue) { + // the startIndex is the String after the first quote of the value after the key + // detect end of key + int startIndex = stringToBeReplaced.indexOf(replacerPattern, 1) + + (replacerPattern.length() + CLOSE_VALUE.length()); + // detect start of value + startIndex = stringToBeReplaced.indexOf(QUOTE, startIndex) + 1; + + // detect end of value + int endIndex = stringToBeReplaced.indexOf(QUOTE, startIndex); + + replacedString.append(stringToBeReplaced.substring(0, startIndex)); + replacedString.append(replacerValue); + + return stringToBeReplaced.substring(endIndex); + } + + static String replaceVins(String stringToBeReplaced) { + String[] vinStrings = stringToBeReplaced.split(VIN_PATTERN); + + StringBuffer replacedString = new StringBuffer(); + replacedString.append(vinStrings[0]); + for (int i = 1; vinStrings.length > 0 && i < vinStrings.length; i++) { + replacedString.append(VIN_PATTERN); + replacedString.append(replaceVin(vinStrings[i])); + } + + return replacedString.toString(); + } + + static String replaceVin(String stringToBeReplaced) { + // the vin is between two quotes + int startIndex = stringToBeReplaced.indexOf(QUOTE) + 1; + int endIndex = stringToBeReplaced.indexOf(QUOTE, startIndex); + + StringBuffer replacedString = new StringBuffer(); + replacedString.append(stringToBeReplaced.substring(0, startIndex)); + replacedString.append(ANONYMOUS_VIN); + replacedString.append(stringToBeReplaced.substring(endIndex)); + + return replacedString.toString(); + } + + static @Nullable String replaceVin(@Nullable String stringToBeReplaced, @Nullable String vin) { + if (stringToBeReplaced == null) { + return null; + } + return vin != null ? stringToBeReplaced.replace(vin, ANONYMOUS_VIN) : stringToBeReplaced; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ByteResponseCallback.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/ExecutionState.java similarity index 60% rename from bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ByteResponseCallback.java rename to bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/ExecutionState.java index a74d0b0b9e2b4..6d3ed01a71717 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/ByteResponseCallback.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/ExecutionState.java @@ -10,17 +10,23 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mybmw.internal.handler; +package org.openhab.binding.mybmw.internal.handler.enums; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link ByteResponseCallback} Interface for all raw byte results from ASYNC REST API - * - * @author Bernd Weymann - Initial contribution + * + * execution state of a remote command + * + * @author Martin Grassl - initial contribution */ @NonNullByDefault -public interface ByteResponseCallback extends ResponseCallback { - - public void onResponse(byte[] result); +public enum ExecutionState { + READY, + INITIATED, + PENDING, + DELIVERED, + EXECUTED, + ERROR, + TIMEOUT } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java new file mode 100644 index 0000000000000..6b3812a6b840c --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.enums; + +import static org.openhab.binding.mybmw.internal.MyBMWConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * + * possible remote services + * + * @author Martin Grassl - initial contribution + * @author Mark Herwege - electric charging commands + */ +@NonNullByDefault +public enum RemoteService { + LIGHT_FLASH("Flash Lights", REMOTE_SERVICE_LIGHT_FLASH, REMOTE_SERVICE_LIGHT_FLASH, ""), + VEHICLE_FINDER("Vehicle Finder", REMOTE_SERVICE_VEHICLE_FINDER, REMOTE_SERVICE_VEHICLE_FINDER, ""), + DOOR_LOCK("Door Lock", REMOTE_SERVICE_DOOR_LOCK, REMOTE_SERVICE_DOOR_LOCK, ""), + DOOR_UNLOCK("Door Unlock", REMOTE_SERVICE_DOOR_UNLOCK, REMOTE_SERVICE_DOOR_UNLOCK, ""), + HORN_BLOW("Horn Blow", REMOTE_SERVICE_HORN, REMOTE_SERVICE_HORN, ""), + CLIMATE_NOW_START("Start Climate", REMOTE_SERVICE_AIR_CONDITIONING_START, "climate-now", "{\"action\": \"START\"}"), + CLIMATE_NOW_STOP("Stop Climate", REMOTE_SERVICE_AIR_CONDITIONING_STOP, "climate-now", "{\"action\": \"STOP\"}"), + CHARGE_NOW("Charge", REMOTE_SERVICE_CHARGE, "charge-now", ""); + + private final String label; + private final String id; + private final String command; + private final String body; + + RemoteService(final String label, final String id, String command, String body) { + this.label = label; + this.id = id; + this.command = command; + this.body = body; + } + + public String getLabel() { + return label; + } + + public String getId() { + return id; + } + + public String getCommand() { + return command; + } + + public String getBody() { + return body; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java deleted file mode 100644 index 4b9299ff5ed73..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/simulation/Injector.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler.simulation; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * The {@link Injector} Simulates feedback of the BMW API - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class Injector { - private static boolean active = false; - - // copy discovery json here - private static String discovery = ""; - - // copy vehicle status json here - private static String status = ""; - - public static boolean isActive() { - return active; - } - - public static String getDiscovery() { - return discovery; - } - - public static String getStatus() { - return status; - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java index 0ae620e26f60b..a2f999f683cf4 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/BimmerConstants.java @@ -25,49 +25,58 @@ * https://customer.bmwgroup.com/one/app/oauth.js * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - update to v2 API */ @NonNullByDefault -public class BimmerConstants { +public interface BimmerConstants { - public static final String REGION_NORTH_AMERICA = "NORTH_AMERICA"; - public static final String REGION_CHINA = "CHINA"; - public static final String REGION_ROW = "ROW"; + static final String REGION_NORTH_AMERICA = "NORTH_AMERICA"; + static final String REGION_CHINA = "CHINA"; + static final String REGION_ROW = "ROW"; - public static final String BRAND_BMW = "bmw"; - public static final String BRAND_MINI = "mini"; - public static final List ALL_BRANDS = List.of(BRAND_BMW, BRAND_MINI); + static final String BRAND_BMW = "bmw"; + static final String BRAND_BMWI = "bmw_i"; + static final String BRAND_MINI = "mini"; + static final List REQUESTED_BRANDS = List.of(BRAND_BMW, BRAND_MINI); - public static final String OAUTH_ENDPOINT = "/gcdm/oauth/authenticate"; + static final String OAUTH_ENDPOINT = "/gcdm/oauth/authenticate"; + static final String AUTH_PROVIDER = "gcdm"; - public static final String EADRAX_SERVER_NORTH_AMERICA = "cocoapi.bmwgroup.us"; - public static final String EADRAX_SERVER_ROW = "cocoapi.bmwgroup.com"; - public static final String EADRAX_SERVER_CHINA = "myprofile.bmw.com.cn"; - public static final Map EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, - EADRAX_SERVER_NORTH_AMERICA, REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW); + static final String EADRAX_SERVER_NORTH_AMERICA = "cocoapi.bmwgroup.us"; + static final String EADRAX_SERVER_ROW = "cocoapi.bmwgroup.com"; + static final String EADRAX_SERVER_CHINA = "myprofile.bmw.com.cn"; + static final Map EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, EADRAX_SERVER_NORTH_AMERICA, + REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW); - public static final String OCP_APIM_KEY_NORTH_AMERICA = "31e102f5-6f7e-7ef3-9044-ddce63891362"; - public static final String OCP_APIM_KEY_ROW = "4f1c85a3-758f-a37d-bbb6-f8704494acfa"; - public static final Map OCP_APIM_KEYS = Map.of(REGION_NORTH_AMERICA, OCP_APIM_KEY_NORTH_AMERICA, + static final String OCP_APIM_KEY_NORTH_AMERICA = "31e102f5-6f7e-7ef3-9044-ddce63891362"; + static final String OCP_APIM_KEY_ROW = "4f1c85a3-758f-a37d-bbb6-f8704494acfa"; + static final Map OCP_APIM_KEYS = Map.of(REGION_NORTH_AMERICA, OCP_APIM_KEY_NORTH_AMERICA, REGION_ROW, OCP_APIM_KEY_ROW); - public static final String CHINA_PUBLIC_KEY = "/eadrax-coas/v1/cop/publickey"; - public static final String CHINA_LOGIN = "/eadrax-coas/v1/login/pwd"; + static final String CHINA_PUBLIC_KEY = "/eadrax-coas/v1/cop/publickey"; + static final String CHINA_LOGIN = "/eadrax-coas/v2/login/pwd"; // Http variables - public static final String USER_AGENT = "Dart/2.14 (dart:io)"; - public static final String X_USER_AGENT = "android(SP1A.210812.016.C1);%s;2.5.2(14945);%s"; + static final String APP_VERSION_NORTH_AMERICA = "2.12.0(19883)"; + static final String APP_VERSION_ROW = "2.12.0(19883)"; + static final String APP_VERSION_CHINA = "2.3.0(13603)"; + static final Map APP_VERSIONS = Map.of(REGION_NORTH_AMERICA, APP_VERSION_NORTH_AMERICA, REGION_ROW, + APP_VERSION_ROW, REGION_CHINA, APP_VERSION_CHINA); + static final String USER_AGENT = "Dart/2.16 (dart:io)"; + // see const.py of bimmer_constants: user-agent; brand; app_version; region + static final String X_USER_AGENT = "android(SP1A.210812.016.C1);%s;%s;%s"; - public static final String LOGIN_NONCE = "login_nonce"; - public static final String AUTHORIZATION_CODE = "authorization_code"; + static final String LOGIN_NONCE = "login_nonce"; + static final String AUTHORIZATION_CODE = "authorization_code"; // Parameters for API Requests - public static final String TIRE_GUARD_MODE = "tireGuardMode"; - public static final String APP_DATE_TIME = "appDateTime"; - public static final String APP_TIMEZONE = "apptimezone"; + static final String TIRE_GUARD_MODE = "tireGuardMode"; + static final String APP_DATE_TIME = "appDateTime"; + static final String APP_TIMEZONE = "apptimezone"; // API endpoints - public static final String API_OAUTH_CONFIG = "/eadrax-ucs/v1/presentation/oauth/config"; - public static final String API_VEHICLES = "/eadrax-vcs/v1/vehicles"; - public static final String API_REMOTE_SERVICE_BASE_URL = "/eadrax-vrccs/v2/presentation/remote-commands/"; // '/{vin}/{service_type}' - public static final String API_POI = "/eadrax-dcs/v1/send-to-car/send-to-car"; + static final String API_OAUTH_CONFIG = "/eadrax-ucs/v1/presentation/oauth/config"; + static final String API_VEHICLES = "/eadrax-vcs/v4/vehicles"; + static final String API_REMOTE_SERVICE_BASE_URL = "/eadrax-vrccs/v3/presentation/remote-commands/"; // '/{vin}/{service_type}' + static final String API_POI = "/eadrax-dcs/v1/send-to-car/send-to-car"; } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java deleted file mode 100644 index 620861fbf87e0..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileWrapper.java +++ /dev/null @@ -1,303 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.utils; - -import static org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey.*; -import static org.openhab.binding.mybmw.internal.utils.Constants.*; - -import java.time.DayOfWeek; -import java.time.LocalTime; -import java.time.format.DateTimeParseException; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mybmw.internal.MyBMWConstants.ChargingMode; -import org.openhab.binding.mybmw.internal.MyBMWConstants.ChargingPreference; -import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile; -import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings; -import org.openhab.binding.mybmw.internal.dto.charge.ChargingWindow; -import org.openhab.binding.mybmw.internal.dto.charge.Time; -import org.openhab.binding.mybmw.internal.dto.charge.Timer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link ChargeProfileWrapper} Wrapper for ChargeProfiles - * - * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - add ChargeProfileActions - */ -@NonNullByDefault -public class ChargeProfileWrapper { - private static final Logger LOGGER = LoggerFactory.getLogger(ChargeProfileWrapper.class); - - private static final String CHARGING_WINDOW = "chargingWindow"; - private static final String WEEKLY_PLANNER = "weeklyPlanner"; - private static final String ACTIVATE = "activate"; - private static final String DEACTIVATE = "deactivate"; - - public enum ProfileKey { - CLIMATE, - TIMER1, - TIMER2, - TIMER3, - TIMER4, - WINDOWSTART, - WINDOWEND - } - - private Optional mode = Optional.empty(); - private Optional preference = Optional.empty(); - private Optional controlType = Optional.empty(); - private Optional chargeSettings = Optional.empty(); - - private final Map enabled = new HashMap<>(); - private final Map times = new HashMap<>(); - private final Map> daysOfWeek = new HashMap<>(); - - public ChargeProfileWrapper(final ChargeProfile profile) { - setPreference(profile.chargingPreference); - setMode(profile.chargingMode); - controlType = Optional.of(profile.chargingControlType); - chargeSettings = Optional.of(profile.chargingSettings); - setEnabled(CLIMATE, profile.climatisationOn); - - addTimer(TIMER1, profile.getTimerId(1)); - addTimer(TIMER2, profile.getTimerId(2)); - if (profile.chargingControlType.equals(WEEKLY_PLANNER)) { - addTimer(TIMER3, profile.getTimerId(3)); - addTimer(TIMER4, profile.getTimerId(4)); - } - - if (CHARGING_WINDOW.equals(profile.chargingPreference)) { - addTime(WINDOWSTART, profile.reductionOfChargeCurrent.start); - addTime(WINDOWEND, profile.reductionOfChargeCurrent.end); - } else { - preference.ifPresent(pref -> { - if (ChargingPreference.chargingWindow.equals(pref)) { - addTime(WINDOWSTART, null); - addTime(WINDOWEND, null); - } - }); - } - } - - public @Nullable Boolean isEnabled(final ProfileKey key) { - return enabled.get(key); - } - - public void setEnabled(final ProfileKey key, @Nullable final Boolean enabled) { - if (enabled == null) { - this.enabled.remove(key); - } else { - this.enabled.put(key, enabled); - } - } - - public @Nullable String getMode() { - return mode.map(m -> m.name()).orElse(null); - } - - public @Nullable String getControlType() { - return controlType.get(); - } - - public @Nullable ChargingSettings getChargeSettings() { - return chargeSettings.get(); - } - - public void setMode(final @Nullable String mode) { - if (mode != null) { - try { - this.mode = Optional.of(ChargingMode.valueOf(mode)); - return; - } catch (IllegalArgumentException iae) { - LOGGER.warn("unexpected value for chargingMode: {}", mode); - } - } - this.mode = Optional.empty(); - } - - public @Nullable String getPreference() { - return preference.map(pref -> pref.name()).orElse(null); - } - - public void setPreference(final @Nullable String preference) { - if (preference != null) { - try { - this.preference = Optional.of(ChargingPreference.valueOf(preference)); - return; - } catch (IllegalArgumentException iae) { - LOGGER.warn("unexpected value for chargingPreference: {}", preference); - } - } - this.preference = Optional.empty(); - } - - public @Nullable Set getDays(final ProfileKey key) { - return daysOfWeek.get(key); - } - - public void setDays(final ProfileKey key, final @Nullable Set 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 days = daysOfWeek.get(key); - if (days == null) { - daysOfWeek.put(key, enabled ? EnumSet.of(day) : EnumSet.noneOf(DayOfWeek.class)); - } else { - if (enabled) { - days.add(day); - } else { - days.remove(day); - } - } - } - - public LocalTime getTime(final ProfileKey key) { - LocalTime t = times.get(key); - if (t != null) { - return t; - } else { - LOGGER.debug("Profile not valid - Key {} doesn't contain boolean value", key); - return Constants.NULL_LOCAL_TIME; - } - } - - public void setTime(final ProfileKey key, @Nullable LocalTime time) { - if (time == null) { - times.remove(key); - } else { - times.put(key, time); - } - } - - public String getJson() { - final ChargeProfile profile = new ChargeProfile(); - - preference.ifPresent(pref -> profile.chargingPreference = pref.name()); - profile.chargingControlType = controlType.get(); - Boolean enabledBool = isEnabled(CLIMATE); - profile.climatisationOn = enabledBool == null ? false : enabledBool; - preference.ifPresent(pref -> { - if (ChargingPreference.chargingWindow.equals(pref)) { - profile.chargingMode = getMode(); - final LocalTime start = getTime(WINDOWSTART); - final LocalTime end = getTime(WINDOWEND); - if (!start.equals(Constants.NULL_LOCAL_TIME) && !end.equals(Constants.NULL_LOCAL_TIME)) { - ChargingWindow cw = new ChargingWindow(); - profile.reductionOfChargeCurrent = cw; - cw.start = new Time(); - cw.start.hour = start.getHour(); - cw.start.minute = start.getMinute(); - cw.end = new Time(); - cw.end.hour = end.getHour(); - cw.end.minute = end.getMinute(); - } - } - }); - profile.departureTimes = new ArrayList(); - profile.departureTimes.add(getTimer(TIMER1)); - profile.departureTimes.add(getTimer(TIMER2)); - if (profile.chargingControlType.equals(WEEKLY_PLANNER)) { - profile.departureTimes.add(getTimer(TIMER3)); - profile.departureTimes.add(getTimer(TIMER4)); - } - - profile.chargingSettings = chargeSettings.get(); - return Converter.getGson().toJson(profile); - } - - private void addTime(final ProfileKey key, @Nullable final Time time) { - try { - times.put(key, time == null ? NULL_LOCAL_TIME : LocalTime.parse(Converter.getTime(time), TIME_FORMATER)); - } catch (DateTimeParseException dtpe) { - LOGGER.warn("unexpected value for {} time: {}", key.name(), time); - } - } - - private void addTimer(final ProfileKey key, @Nullable final Timer timer) { - if (timer == null) { - enabled.put(key, false); - addTime(key, null); - daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class)); - } else { - enabled.put(key, ACTIVATE.equals(timer.action)); - addTime(key, timer.timeStamp); - final EnumSet daySet = EnumSet.noneOf(DayOfWeek.class); - if (timer.timerWeekDays != null) { - daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class)); - for (String day : timer.timerWeekDays) { - try { - daySet.add(DayOfWeek.valueOf(day.toUpperCase())); - } catch (IllegalArgumentException iae) { - LOGGER.warn("unexpected value for {} day: {}", key.name(), day); - } - daysOfWeek.put(key, daySet); - } - } - } - } - - private Timer getTimer(final ProfileKey key) { - final Timer timer = new Timer(); - switch (key) { - case TIMER1: - timer.id = 1; - break; - case TIMER2: - timer.id = 2; - break; - case TIMER3: - timer.id = 3; - break; - case TIMER4: - timer.id = 4; - break; - default: - // timer id stays -1 - break; - } - Boolean enabledBool = isEnabled(key); - if (enabledBool != null) { - timer.action = enabledBool ? ACTIVATE : DEACTIVATE; - } else { - timer.action = DEACTIVATE; - } - final LocalTime time = getTime(key); - if (!time.equals(Constants.NULL_LOCAL_TIME)) { - timer.timeStamp = new Time(); - timer.timeStamp.hour = time.getHour(); - timer.timeStamp.minute = time.getMinute(); - } - final Set days = daysOfWeek.get(key); - if (days != null) { - timer.timerWeekDays = new ArrayList<>(); - for (DayOfWeek day : days) { - timer.timerWeekDays.add(day.name().toLowerCase()); - } - } - return timer; - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileUtils.java similarity index 96% rename from bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileUtils.java rename to bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileUtils.java index 3df2c701c9d18..194bf6732b8a9 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargeProfileUtils.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileUtils.java @@ -22,15 +22,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey; +import org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper.ProfileKey; /** - * The {@link ChargeProfileUtils} utility functions for charging profiles + * The {@link ChargingProfileUtils} utility functions for charging profiles * * @author Norbert Truchsess - initial contribution */ @NonNullByDefault -public class ChargeProfileUtils { +public class ChargingProfileUtils { // Charging public static class TimedChannel { diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileWrapper.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileWrapper.java new file mode 100644 index 0000000000000..10f04174863ed --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileWrapper.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.utils; + +import static org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper.ProfileKey.TIMER1; +import static org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper.ProfileKey.TIMER2; +import static org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper.ProfileKey.TIMER3; +import static org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper.ProfileKey.TIMER4; +import static org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper.ProfileKey.WINDOWEND; +import static org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper.ProfileKey.WINDOWSTART; +import static org.openhab.binding.mybmw.internal.utils.Constants.NULL_LOCAL_TIME; +import static org.openhab.binding.mybmw.internal.utils.Constants.TIME_FORMATER; + +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mybmw.internal.MyBMWConstants.ChargingMode; +import org.openhab.binding.mybmw.internal.MyBMWConstants.ChargingPreference; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingProfile; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings; +import org.openhab.binding.mybmw.internal.dto.charge.Time; +import org.openhab.binding.mybmw.internal.dto.charge.Timer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ChargingProfileWrapper} Wrapper for ChargingProfiles + * + * @author Bernd Weymann - Initial contribution + * @author Norbert Truchsess - add ChargeProfileActions + * @author Martin Grassl - refactoring + */ +@NonNullByDefault +public class ChargingProfileWrapper { + private final Logger logger = LoggerFactory.getLogger(ChargingProfileWrapper.class); + + private static final String CHARGING_WINDOW = "CHARGING_WINDOW"; + private static final String WEEKLY_PLANNER = "WEEKLY_PLANNER"; + private static final String ACTIVATE = "ACTIVATE"; + // not used private static final String DEACTIVATE = "DEACTIVATE"; + + public enum ProfileKey { + CLIMATE, + TIMER1, + TIMER2, + TIMER3, + TIMER4, + WINDOWSTART, + WINDOWEND + } + + private Optional mode = Optional.empty(); + private Optional preference = Optional.empty(); + private Optional controlType = Optional.empty(); + private Optional chargeSettings = Optional.empty(); + + private final Map enabled = new HashMap<>(); + private final Map times = new HashMap<>(); + private final Map> daysOfWeek = new HashMap<>(); + + public ChargingProfileWrapper(final ChargingProfile profile) { + setPreference(profile.getChargingPreference()); + setMode(profile.getChargingMode()); + controlType = Optional.of(profile.getChargingControlType()); + chargeSettings = Optional.of(profile.getChargingSettings()); + setEnabled(ProfileKey.CLIMATE, profile.isClimatisationOn()); + + addTimer(TIMER1, profile.getTimerId(1)); + addTimer(TIMER2, profile.getTimerId(2)); + if (profile.getChargingControlType().equals(WEEKLY_PLANNER)) { + addTimer(TIMER3, profile.getTimerId(3)); + addTimer(TIMER4, profile.getTimerId(4)); + } + + if (CHARGING_WINDOW.equals(profile.getChargingPreference())) { + addTime(WINDOWSTART, profile.getReductionOfChargeCurrent().getStart()); + addTime(WINDOWEND, profile.getReductionOfChargeCurrent().getEnd()); + } else { + preference.ifPresent(pref -> { + if (ChargingPreference.CHARGING_WINDOW.equals(pref)) { + addTime(WINDOWSTART, null); + addTime(WINDOWEND, null); + } + }); + } + } + + public @Nullable Boolean isEnabled(final ProfileKey key) { + return enabled.get(key); + } + + public void setEnabled(final ProfileKey key, @Nullable final Boolean enabled) { + if (enabled == null) { + this.enabled.remove(key); + } else { + this.enabled.put(key, enabled); + } + } + + public @Nullable String getMode() { + return mode.map(m -> m.name()).orElse(null); + } + + public @Nullable String getControlType() { + return controlType.get(); + } + + public @Nullable ChargingSettings getChargingSettings() { + return chargeSettings.get(); + } + + public void setMode(final @Nullable String mode) { + if (mode != null) { + try { + this.mode = Optional.of(ChargingMode.valueOf(mode)); + return; + } catch (IllegalArgumentException iae) { + logger.warn("unexpected value for chargingMode: {}", mode); + } + } + this.mode = Optional.empty(); + } + + public @Nullable String getPreference() { + return preference.map(pref -> pref.name()).orElse(null); + } + + public void setPreference(final @Nullable String preference) { + if (preference != null) { + try { + this.preference = Optional.of(ChargingPreference.valueOf(preference)); + return; + } catch (IllegalArgumentException iae) { + logger.warn("unexpected value for chargingPreference: {}", preference); + } + } + this.preference = Optional.empty(); + } + + public @Nullable Set getDays(final ProfileKey key) { + return daysOfWeek.get(key); + } + + public LocalTime getTime(final ProfileKey key) { + LocalTime t = times.get(key); + if (t != null) { + return t; + } else { + logger.debug("Profile not valid - Key {} doesn't contain boolean value", key); + return Constants.NULL_LOCAL_TIME; + } + } + + private void addTime(final ProfileKey key, @Nullable final Time time) { + try { + times.put(key, time == null ? NULL_LOCAL_TIME : LocalTime.parse(Converter.getTime(time), TIME_FORMATER)); + } catch (DateTimeParseException dtpe) { + logger.warn("unexpected value for {} time: {}", key.name(), time); + } + } + + private void addTimer(final ProfileKey key, @Nullable final Timer timer) { + if (timer == null) { + enabled.put(key, false); + addTime(key, null); + daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class)); + } else { + enabled.put(key, ACTIVATE.equals(timer.action)); + addTime(key, timer.timeStamp); + final EnumSet daySet = EnumSet.noneOf(DayOfWeek.class); + if (timer.timerWeekDays != null) { + daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class)); + for (String day : timer.timerWeekDays) { + try { + daySet.add(DayOfWeek.valueOf(day.toUpperCase())); + } catch (IllegalArgumentException iae) { + logger.warn("unexpected value for {} day: {}", key.name(), day); + } + daysOfWeek.put(key, daySet); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java index 8580f65670f9b..b7ca759e8a3ed 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Constants.java @@ -32,6 +32,7 @@ * * @author Bernd Weymann - Initial contribution * @author Norbert Truchsess - contributor + * @author Martin Grassl - rename drivetrain options */ @NonNullByDefault public class Constants { @@ -92,11 +93,11 @@ public class Constants { }; // Drive Train definitions from json - public static final String BEV = "ELECTRIC"; - public static final String REX_EXTENSION = "(+ REX)"; - public static final String HYBRID = "HYBRID"; - public static final String CONV = "COMBUSTION"; - public static final String PHEV = "PLUGIN_HYBRID"; + public static final String DRIVETRAIN_BEV = "ELECTRIC"; + public static final String DRIVETRAIN_REX_EXTENSION = "(+ REX)"; + public static final String DRIVETRAIN_MILD_HYBRID = "MILD_HYBRID"; + public static final String DRIVETRAIN_CONV = "COMBUSTION"; + public static final String DRIVETRAIN_PHEV = "PLUGIN_HYBRID"; // Carging States public static final String DEFAULT = "DEFAULT"; diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java index 8bcd38356bae0..0451d6c626a7e 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java @@ -12,90 +12,78 @@ */ package org.openhab.binding.mybmw.internal.utils; -import java.lang.reflect.Type; import java.text.SimpleDateFormat; -import java.time.LocalTime; import java.time.ZoneId; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.Locale; import java.util.Random; import java.util.TimeZone; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mybmw.internal.MyBMWConstants; import org.openhab.binding.mybmw.internal.dto.charge.Time; -import org.openhab.binding.mybmw.internal.dto.properties.Address; -import org.openhab.binding.mybmw.internal.dto.properties.Coordinates; -import org.openhab.binding.mybmw.internal.dto.properties.Distance; -import org.openhab.binding.mybmw.internal.dto.properties.Location; -import org.openhab.binding.mybmw.internal.dto.properties.Range; -import org.openhab.binding.mybmw.internal.dto.status.Mileage; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.StringType; 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; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; -import com.google.gson.reflect.TypeToken; - /** * The {@link Converter} Conversion Helpers * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - extract some methods to other classes */ @NonNullByDefault -public class Converter { - public static final Logger LOGGER = LoggerFactory.getLogger(Converter.class); +public interface Converter { + static final Logger LOGGER = LoggerFactory.getLogger(Converter.class); - public static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss"; - public static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); - public static final DateTimeFormatter LOCALE_ENGLISH_TIMEFORMATTER = DateTimeFormatter.ofPattern("hh:mm a", + static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss"; + static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); + static final DateTimeFormatter LOCALE_ENGLISH_TIMEFORMATTER = DateTimeFormatter.ofPattern("hh:mm a", Locale.ENGLISH); - public static final SimpleDateFormat ISO_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"); - - private static final Gson GSON = new Gson(); - private static final Vehicle INVALID_VEHICLE = new Vehicle(); - private static final String SPLIT_HYPHEN = "-"; - private static final String SPLIT_BRACKET = "\\("; - private static final String VIN_PATTERN = "\"vin\":"; - private static final String VEHICLE_LOCATION_PATTERN = "\"vehicleLocation\":"; - private static final String VEHICLE_LOCATION_REPLACEMENT = "\"vehicleLocation\": {\"coordinates\": {\"latitude\": 1.1,\"longitude\": 2.2},\"address\": {\"formatted\": \"anonymous\"},\"heading\": -1}"; - private static final char OPEN_BRACKET = "{".charAt(0); - private static final char CLOSING_BRACKET = "}".charAt(0); + static final SimpleDateFormat ISO_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"); - // https://www.baeldung.com/gson-list - public static final Type VEHICLE_LIST_TYPE = new TypeToken>() { - }.getType(); - public static int offsetMinutes = -1; + static final String SPLIT_HYPHEN = "-"; + static final String SPLIT_BRACKET = "\\("; - public static String zonedToLocalDateTime(String input) { - try { - ZonedDateTime d = ZonedDateTime.parse(input).withZoneSameInstant(ZoneId.systemDefault()); - return d.toLocalDateTime().format(Converter.DATE_INPUT_PATTERN); - } catch (Exception e) { - LOGGER.debug("Unable to parse date {} - {}", input, e.getMessage()); + static State zonedToLocalDateTime(@Nullable String input) { + if (input != null && !input.isEmpty()) { + try { + String dateString = ZonedDateTime.parse(input).withZoneSameInstant(ZoneId.systemDefault()) + .toLocalDateTime().format(Converter.DATE_INPUT_PATTERN); + return DateTimeType.valueOf(dateString); + } catch (Exception e) { + LOGGER.debug("Unable to parse date {} - {}", input, e.getMessage()); + return UnDefType.UNDEF; + } + } else { + return UnDefType.UNDEF; } - return input; } - public static String toTitleCase(@Nullable String input) { - if (input == null) { + /** + * converts a string into a unified format + * - string is Capitalized + * - null is empty string + * - single character remains + * + * @param input + * @return + */ + static String toTitleCase(@Nullable String input) { + if (input == null || input.isEmpty()) { return toTitleCase(Constants.UNDEF); } else if (input.length() == 1) { return input; } else { + // first, replace all underscores with spaces and make it lower case 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); @@ -104,7 +92,9 @@ public static String toTitleCase(@Nullable String input) { } private static String toTitleCase(String input, String splitter) { + // first, split all parts by the splitting string String[] arr = input.split(splitter); + StringBuilder sb = new StringBuilder(); for (int i = 0; i < arr.length; i++) { if (i > 0) { @@ -115,14 +105,6 @@ private static String toTitleCase(String input, String splitter) { 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 * @@ -132,7 +114,7 @@ public static Gson getGson() { * @param destinationLongitude * @return distance */ - public static double measureDistance(double sourceLatitude, double sourceLongitude, double destinationLatitude, + 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; @@ -145,26 +127,38 @@ public static double measureDistance(double sourceLatitude, double sourceLongitu /** * 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 + * 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 + * 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 + * 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 + * 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. + * In future it's foreseen to replace this with BMW RangeMap Service which isn't + * running at the moment. * * @param range * @return mapping from air-line distance to "real road" distance */ - public static int guessRangeRadius(double range) { + static int guessRangeRadius(double range) { return (int) (range * 0.8); } - public static int getIndex(String fullString) { + /** + * checks if a string is a valid integer + * + * @param fullString + * @return + */ + static int parseIntegerString(String fullString) { int index = -1; try { index = Integer.parseInt(fullString); @@ -173,82 +167,7 @@ public static int getIndex(String fullString) { return index; } - /** - * Returns list of found vehicles - * In case of errors return empty list - * - * @param json - * @return - */ - public static List getVehicleList(String json) { - try { - List l = GSON.fromJson(json, VEHICLE_LIST_TYPE); - if (l != null) { - return l; - } else { - return new ArrayList(); - } - } catch (JsonSyntaxException e) { - LOGGER.warn("JsonSyntaxException {}", e.getMessage()); - return new ArrayList(); - } - } - - public static Vehicle getVehicle(String vin, String json) { - List l = getVehicleList(json); - for (Vehicle vehicle : l) { - if (vin.equals(vehicle.vin)) { - // declare vehicle as valid - vehicle.valid = true; - return getConsistentVehcile(vehicle); - } - } - return INVALID_VEHICLE; - } - - public static String getRawVehicleContent(String vin, String json) { - JsonArray jArr = JsonParser.parseString(json).getAsJsonArray(); - for (int i = 0; i < jArr.size(); i++) { - JsonObject jo = jArr.get(i).getAsJsonObject(); - String jsonVin = jo.getAsJsonPrimitive(MyBMWConstants.VIN).getAsString(); - if (vin.equals(jsonVin)) { - return jo.toString(); - } - } - return Constants.EMPTY_JSON; - } - - /** - * ensure basic data like mileage and location data are available every time - * - * @param v - * @return - */ - public static Vehicle getConsistentVehcile(Vehicle v) { - if (v.status.currentMileage == null) { - v.status.currentMileage = new Mileage(); - v.status.currentMileage.mileage = -1; - v.status.currentMileage.units = "km"; - } - if (v.properties.combustionRange == null) { - v.properties.combustionRange = new Range(); - v.properties.combustionRange.distance = new Distance(); - v.properties.combustionRange.distance.value = -1; - v.properties.combustionRange.distance.units = Constants.EMPTY; - } - if (v.properties.vehicleLocation == null) { - v.properties.vehicleLocation = new Location(); - v.properties.vehicleLocation.heading = Constants.INT_UNDEF; - v.properties.vehicleLocation.coordinates = new Coordinates(); - v.properties.vehicleLocation.coordinates.latitude = Constants.INT_UNDEF; - v.properties.vehicleLocation.coordinates.longitude = Constants.INT_UNDEF; - v.properties.vehicleLocation.address = new Address(); - v.properties.vehicleLocation.address.formatted = Constants.UNDEF; - } - return v; - } - - public static String getRandomString(int size) { + static String getRandomString(int size) { int leftLimit = 97; // letter 'a' int rightLimit = 122; // letter 'z' Random random = new Random(); @@ -259,23 +178,7 @@ public static String getRandomString(int size) { return generatedString; } - public static State getLockState(boolean lock) { - if (lock) { - return StringType.valueOf(Constants.LOCKED); - } else { - return StringType.valueOf(Constants.UNLOCKED); - } - } - - public static State getClosedState(boolean close) { - if (close) { - return StringType.valueOf(Constants.CLOSED); - } else { - return StringType.valueOf(Constants.OPEN); - } - } - - public static State getConnectionState(boolean connected) { + static State getConnectionState(boolean connected) { if (connected) { return StringType.valueOf(Constants.CONNECTED); } else { @@ -283,7 +186,7 @@ public static State getConnectionState(boolean connected) { } } - public static String getCurrentISOTime() { + static String getCurrentISOTime() { Date date = new Date(System.currentTimeMillis()); synchronized (ISO_FORMATTER) { ISO_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -291,81 +194,16 @@ public static String getCurrentISOTime() { } } - public static String getTime(Time t) { + static String getTime(Time t) { StringBuffer time = new StringBuffer(); - if (t.hour < 10) { + if (t.getHour() < 10) { time.append("0"); } - time.append(Integer.toString(t.hour)).append(":"); - if (t.minute < 10) { + time.append(Integer.toString(t.getHour())).append(":"); + if (t.getMinute() < 10) { time.append("0"); } - time.append(Integer.toString(t.minute)); + time.append(Integer.toString(t.getMinute())); return time.toString(); } - - public static int getOffsetMinutes() { - if (offsetMinutes == -1) { - ZoneOffset zo = ZonedDateTime.now().getOffset(); - offsetMinutes = zo.getTotalSeconds() / 60; - } - return offsetMinutes; - } - - public static int stringToInt(String intStr) { - int integer = Constants.INT_UNDEF; - try { - integer = Integer.parseInt(intStr); - - } catch (Exception e) { - LOGGER.debug("Unable to convert range {} into int value", intStr); - } - return integer; - } - - public static String getLocalTime(String chrageInfoLabel) { - String[] timeSplit = chrageInfoLabel.split(Constants.TILDE); - if (timeSplit.length == 2) { - try { - LocalTime timeL = LocalTime.parse(timeSplit[1].trim(), LOCALE_ENGLISH_TIMEFORMATTER); - return timeSplit[0] + Constants.TILDE + timeL.toString(); - } catch (Exception e) { - LOGGER.debug("Unable to parse date {} - {}", timeSplit[1], e.getMessage()); - } - } - return chrageInfoLabel; - } - - public static String anonymousFingerprint(String raw) { - String anonymousFingerprintString = raw; - int vinStartIndex = raw.indexOf(VIN_PATTERN); - if (vinStartIndex != -1) { - String[] arr = raw.substring(vinStartIndex + VIN_PATTERN.length()).trim().split("\""); - String vin = arr[1].trim(); - anonymousFingerprintString = raw.replace(vin, "anonymous"); - } - - int locationStartIndex = raw.indexOf(VEHICLE_LOCATION_PATTERN); - int bracketCounter = -1; - if (locationStartIndex != -1) { - int endLocationIndex = 0; - for (int i = locationStartIndex; i < raw.length() && bracketCounter != 0; i++) { - endLocationIndex = i; - if (raw.charAt(i) == OPEN_BRACKET) { - if (bracketCounter == -1) { - // start point - bracketCounter = 1; - } else { - bracketCounter++; - } - } else if (raw.charAt(i) == CLOSING_BRACKET) { - bracketCounter--; - } - } - String locationReplacement = raw.substring(locationStartIndex, endLocationIndex + 1); - anonymousFingerprintString = anonymousFingerprintString.replace(locationReplacement, - VEHICLE_LOCATION_REPLACEMENT); - } - return anonymousFingerprintString; - } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java index 38e8fba7729aa..68226eae3d729 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java @@ -18,36 +18,43 @@ * The {@link HTTPConstants} class contains fields mapping thing configuration parameters. * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - added image content type */ @NonNullByDefault -public class HTTPConstants { - public static final int HTTP_TIMEOUT_SEC = 10; +public interface HTTPConstants { + static final int HTTP_TIMEOUT_SEC = 10; - public static final String CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded"; - public static final String CONTENT_TYPE_JSON_ENCODED = "application/json"; - public static final String KEEP_ALIVE = "Keep-Alive"; - public static final String CLIENT_ID = "client_id"; - public static final String RESPONSE_TYPE = "response_type"; - public static final String TOKEN = "token"; - public static final String CODE = "code"; - public static final String CODE_VERIFIER = "code_verifier"; - public static final String STATE = "state"; - public static final String NONCE = "nonce"; - public static final String REDIRECT_URI = "redirect_uri"; - public static final String AUTHORIZATION = "authorization"; - public static final String GRANT_TYPE = "grant_type"; - public static final String SCOPE = "scope"; - public static final String CREDENTIALS = "Credentials"; - public static final String USERNAME = "username"; - public static final String PASSWORD = "password"; - public static final String CONTENT_LENGTH = "Content-Length"; - public static final String CODE_CHALLENGE = "code_challenge"; - public static final String CODE_CHALLENGE_METHOD = "code_challenge_method"; - public static final String ACCESS_TOKEN = "access_token"; - public static final String TOKEN_TYPE = "token_type"; - public static final String EXPIRES_IN = "expires_in"; - public static final String CHUNKED = "chunked"; + static final String CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded"; + static final String CONTENT_TYPE_JSON = "application/json"; + static final String CONTENT_TYPE_IMAGE = "image/png"; + static final String KEEP_ALIVE = "Keep-Alive"; + static final String CLIENT_ID = "client_id"; + static final String RESPONSE_TYPE = "response_type"; + static final String TOKEN = "token"; + static final String CODE = "code"; + static final String CODE_VERIFIER = "code_verifier"; + static final String STATE = "state"; + static final String NONCE = "nonce"; + static final String REDIRECT_URI = "redirect_uri"; + static final String AUTHORIZATION = "authorization"; + static final String GRANT_TYPE = "grant_type"; + static final String SCOPE = "scope"; + static final String CREDENTIALS = "Credentials"; + static final String USERNAME = "username"; + static final String PASSWORD = "password"; + static final String CONTENT_LENGTH = "Content-Length"; + static final String CODE_CHALLENGE = "code_challenge"; + static final String CODE_CHALLENGE_METHOD = "code_challenge_method"; + static final String ACCESS_TOKEN = "access_token"; + static final String TOKEN_TYPE = "token_type"; + static final String EXPIRES_IN = "expires_in"; + static final String CHUNKED = "chunked"; - public static final String ACP_SUBSCRIPTION_KEY = "ocp-apim-subscription-key"; - public static final String X_USER_AGENT = "x-user-agent"; + // HTTP headers for BMW API + static final String HEADER_ACP_SUBSCRIPTION_KEY = "ocp-apim-subscription-key"; + static final String HEADER_X_USER_AGENT = "x-user-agent"; + static final String HEADER_X_IDENTITY_PROVIDER = "x-identity-provider"; + static final String HEADER_X_CORRELATION_ID = "x-correlation-id"; + static final String HEADER_BMW_CORRELATION_ID = "bmw-correlation-id"; + static final String HEADER_BMW_VIN = "bmw-vin"; } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java index cabdcc665495e..b1a519028eca4 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ImageProperties.java @@ -18,12 +18,13 @@ * The {@link ImageProperties} Properties of current Vehicle Image * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - fix default viewport as "default" is not available anymore */ @NonNullByDefault public class ImageProperties { public static final int RETRY_COUNTER = 5; public int failCounter = 0; - public String viewport = "Default"; + public String viewport = "VehicleStatus"; public ImageProperties(String viewport) { this.viewport = viewport; diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java new file mode 100644 index 0000000000000..9c9dc28200e79 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.utils; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; + +/** + * + * checks if the configuration is valid + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - extracted to own class + */ +@NonNullByDefault +public final class MyBMWConfigurationChecker { + public static boolean checkConfiguration(MyBMWBridgeConfiguration config) { + if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) { + return false; + } else { + return BimmerConstants.EADRAX_SERVER_MAP.containsKey(config.region); + } + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java index 2b9ff08074991..29cc02a72464b 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/RemoteServiceUtils.java @@ -19,13 +19,14 @@ import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.mybmw.internal.handler.RemoteServiceHandler.RemoteService; +import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; import org.openhab.core.types.CommandOption; /** * Helper class for Remote Service Commands * * @author Norbert Truchsess - Initial contribution + * @author Martin Grassl - small refactoring */ @NonNullByDefault public class RemoteServiceUtils { @@ -33,7 +34,7 @@ public class RemoteServiceUtils { private static final Map COMMAND_SERVICES = Stream.of(RemoteService.values()) .collect(Collectors.toUnmodifiableMap(RemoteService::getId, service -> service)); - public static Optional getRemoteService(final String command) { + public static Optional getRemoteServiceFromCommand(final String command) { return Optional.ofNullable(COMMAND_SERVICES.get(command)); } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java index 0b2d862ea44ef..16c9d605bfd79 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java @@ -15,18 +15,11 @@ import java.time.ZonedDateTime; import java.util.List; -import javax.measure.Unit; -import javax.measure.quantity.Length; - import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType; -import org.openhab.binding.mybmw.internal.dto.properties.CBS; -import org.openhab.binding.mybmw.internal.dto.status.FuelIndicator; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.dto.vehicle.RequiredService; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; @@ -36,17 +29,24 @@ * The {@link VehicleStatusUtils} Data Transfer Object * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactor for v2 and extract some methods to other classes */ @NonNullByDefault public class VehicleStatusUtils { public static final Logger LOGGER = LoggerFactory.getLogger(VehicleStatusUtils.class); - public static State getNextServiceDate(List cbsMessageList) { + /** + * the date can be empty + * + * @param requiredServices + * @return + */ + public static State getNextServiceDate(List requiredServices) { ZonedDateTime farFuture = ZonedDateTime.now().plusYears(100); ZonedDateTime serviceDate = farFuture; - for (CBS service : cbsMessageList) { - if (service.dateTime != null) { - ZonedDateTime d = ZonedDateTime.parse(service.dateTime); + for (RequiredService requiredService : requiredServices) { + if (requiredService.getDateTime() != null && !requiredService.getDateTime().isEmpty()) { + ZonedDateTime d = ZonedDateTime.parse(requiredService.getDateTime()); if (d.isBefore(serviceDate)) { serviceDate = d; } // else skip @@ -60,23 +60,23 @@ public static State getNextServiceDate(List cbsMessageList) { } } - public static State getNextServiceMileage(List cbsMessageList) { - boolean imperial = false; + /** + * the mileage can be empty + * + * @param requiredServices + * @return + */ + public static State getNextServiceMileage(List requiredServices) { int serviceMileage = Integer.MAX_VALUE; - for (CBS service : cbsMessageList) { - if (service.distance != null) { - if (service.distance.value < serviceMileage) { - serviceMileage = service.distance.value; - imperial = !Constants.KILOMETERS_JSON.equals(service.distance.units); + for (RequiredService requiredService : requiredServices) { + if (requiredService.getMileage() > 0) { + if (requiredService.getMileage() < serviceMileage) { + serviceMileage = requiredService.getMileage(); } } } if (serviceMileage != Integer.MAX_VALUE) { - if (imperial) { - return QuantityType.valueOf(serviceMileage, ImperialUnits.MILE); - } else { - return QuantityType.valueOf(serviceMileage, Constants.KILOMETRE_UNIT); - } + return QuantityType.valueOf(serviceMileage, Constants.KILOMETRE_UNIT); } else { return UnDefType.UNDEF; } @@ -90,152 +90,19 @@ public static State getNextServiceMileage(List cbsMessageList) { * @return */ public static VehicleType vehicleType(String driveTrain, String model) { - if (Constants.BEV.equals(driveTrain)) { - if (model.endsWith(Constants.REX_EXTENSION)) { + if (Constants.DRIVETRAIN_BEV.equals(driveTrain)) { + if (model.endsWith(Constants.DRIVETRAIN_REX_EXTENSION)) { return VehicleType.ELECTRIC_REX; } else { return VehicleType.ELECTRIC; } - } else if (Constants.PHEV.equals(driveTrain)) { + } else if (Constants.DRIVETRAIN_PHEV.equals(driveTrain)) { return VehicleType.PLUGIN_HYBRID; - } else if (Constants.CONV.equals(driveTrain) || Constants.HYBRID.equals(driveTrain)) { + } else if (Constants.DRIVETRAIN_CONV.equals(driveTrain) + || Constants.DRIVETRAIN_MILD_HYBRID.equals(driveTrain)) { return VehicleType.CONVENTIONAL; } LOGGER.warn("Unknown Vehicle Type: {} | {}", model, driveTrain); return VehicleType.UNKNOWN; } - - public static @Nullable Unit getLengthUnit(List indicators) { - Unit ret = null; - for (FuelIndicator fuelIndicator : indicators) { - String unitAbbrev = fuelIndicator.rangeUnits; - switch (unitAbbrev) { - case Constants.KM_JSON: - if (ret != null) { - if (!ret.equals(Constants.KILOMETRE_UNIT)) { - LOGGER.debug("Ambigious Unit declarations. Found {} before {}", ret, Constants.KM_JSON); - } // else - fine! - } else { - ret = Constants.KILOMETRE_UNIT; - } - break; - case Constants.MI_JSON: - if (ret != null) { - if (!ret.equals(ImperialUnits.MILE)) { - LOGGER.debug("Ambigious Unit declarations. Found {} before {}", ret, Constants.MI_JSON); - } // else - fine! - } else { - ret = ImperialUnits.MILE; - } - break; - default: - LOGGER.debug("Cannot evaluate Unit for {}", unitAbbrev); - break; - } - } - return ret; - } - - /** - * The range values delivered by BMW are quite ambiguous! - * - status fuel indicators are missing a unique identifier - * - properties ranges delivering wrong values for hybrid and fuel range - * - properties ranges are not reflecting mi / km - every time km - * - * So getRange will try - * 1) fuel indicator - * 2) ranges from properties, except combined range - * 3) take a guess from fuel indicators - * - * @param unitJson - * @param indicators - * @return - */ - public static int getRange(String unitJson, Vehicle vehicle) { - if (vehicle.status.fuelIndicators.size() == 1) { - return Converter.stringToInt(vehicle.status.fuelIndicators.get(0).rangeValue); - } else { - return guessRange(unitJson, vehicle); - } - } - - /** - * Guesses the range from 3 fuelindicators - * - electric range calculation is correct - * - for the 2 other values: - * -- smaller one is assigned to fuel range - * -- bigger one is assigned to hybrid range - * - * @see VehicleStatusTest testGuessRange - * - * @param unitJson - * @param vehicle - * @return - */ - public static int guessRange(String unitJson, Vehicle vehicle) { - int electricGuess = Constants.INT_UNDEF; - int fuelGuess = Constants.INT_UNDEF; - int hybridGuess = Constants.INT_UNDEF; - for (FuelIndicator fuelIndicator : vehicle.status.fuelIndicators) { - // electric range - this fits 100% - if (Constants.UNIT_PRECENT_JSON.equals(fuelIndicator.levelUnits) - && fuelIndicator.chargingStatusType != null) { - // found electric - electricGuess = Converter.stringToInt(fuelIndicator.rangeValue); - } else { - if (fuelGuess == Constants.INT_UNDEF) { - // fuel not set? then assume it's fuel - fuelGuess = Converter.stringToInt(fuelIndicator.rangeValue); - } else { - // fuel already guessed - take smaller value for fuel, bigger for hybrid - int newGuess = Converter.stringToInt(fuelIndicator.rangeValue); - hybridGuess = Math.max(fuelGuess, newGuess); - fuelGuess = Math.min(fuelGuess, newGuess); - } - } - } - switch (unitJson) { - case Constants.UNIT_PRECENT_JSON: - return electricGuess; - case Constants.UNIT_LITER_JSON: - return fuelGuess; - case Constants.PHEV: - return hybridGuess; - default: - return Constants.INT_UNDEF; - } - } - - public static String getChargStatus(Vehicle vehicle) { - FuelIndicator fi = getElectricFuelIndicator(vehicle); - if (fi.chargingStatusType != null) { - if (fi.chargingStatusType.equals(Constants.DEFAULT)) { - return Constants.NOT_CHARGING_STATE; - } else { - return fi.chargingStatusType; - } - } - return Constants.UNDEF; - } - - public static String getChargeInfo(Vehicle vehicle) { - FuelIndicator fi = getElectricFuelIndicator(vehicle); - if (fi.chargingStatusType != null && fi.infoLabel != null) { - if (fi.chargingStatusType.equals(Constants.CHARGING_STATE) - || fi.chargingStatusType.equals(Constants.PLUGGED_STATE)) { - return fi.infoLabel; - } - } - return Constants.HYPHEN; - } - - private static FuelIndicator getElectricFuelIndicator(Vehicle vehicle) { - for (FuelIndicator fuelIndicator : vehicle.status.fuelIndicators) { - if (Constants.UNIT_PRECENT_JSON.equals(fuelIndicator.levelUnits) - && fuelIndicator.chargingStatusType != null) { - return fuelIndicator; - } - } - return new FuelIndicator(); - } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties index 2cbf443a25180..b2435c5eea16f 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties @@ -1,105 +1,106 @@ -# add-on - -addon.mybmw.name = MyBMW -addon.mybmw.description = Provides access to your Vehicle Data like MyBMW App +# Binding +binding.mybmw.name = MyBMW +binding.mybmw.description = Provides access to your Vehicle Data like MyBMW App # thing types - -thing-type.mybmw.account.label = MyBMW Account -thing-type.mybmw.account.description = Your BMW account data -thing-type.mybmw.bev.label = Electric Vehicle -thing-type.mybmw.bev.description = Battery Electric Vehicle (BEV) -thing-type.mybmw.bev_rex.label = Electric Vehicle with REX -thing-type.mybmw.bev_rex.description = Battery Electric Vehicle with Range Extender (BEV_REX) -thing-type.mybmw.conv.label = Conventional Vehicle -thing-type.mybmw.conv.description = Conventional Fuel Vehicle (CONV) -thing-type.mybmw.phev.label = Plug-In-Hybrid Electric Vehicle -thing-type.mybmw.phev.description = Conventional Fuel Vehicle with supporting Electric Engine (PHEV) - -# thing types config - -thing-type.config.mybmw.bridge.language.label = Language Settings thing-type.config.mybmw.bridge.language.description = Channel data can be returned in the desired language like en, de, fr ... -thing-type.config.mybmw.bridge.password.label = Password +thing-type.config.mybmw.bridge.language.label = Language Settings thing-type.config.mybmw.bridge.password.description = MyBMW Password -thing-type.config.mybmw.bridge.region.label = Region +thing-type.config.mybmw.bridge.password.label = Password thing-type.config.mybmw.bridge.region.description = Select Region in order to connect to the appropriate BMW Server -thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = North America +thing-type.config.mybmw.bridge.region.label = Region thing-type.config.mybmw.bridge.region.option.CHINA = China +thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = North America thing-type.config.mybmw.bridge.region.option.ROW = Rest of the World -thing-type.config.mybmw.bridge.userName.label = Username thing-type.config.mybmw.bridge.userName.description = MyBMW Username -thing-type.config.mybmw.vehicle.refreshInterval.label = Refresh Interval +thing-type.config.mybmw.bridge.userName.label = Username + thing-type.config.mybmw.vehicle.refreshInterval.description = Data refresh rate for your vehicle data -thing-type.config.mybmw.vehicle.vehicleBrand.label = Brand of the Vehicle +thing-type.config.mybmw.vehicle.refreshInterval.label = Refresh Interval thing-type.config.mybmw.vehicle.vehicleBrand.description = Vehicle brand like BMW or Mini -thing-type.config.mybmw.vehicle.vin.label = Vehicle Identification Number (VIN) +thing-type.config.mybmw.vehicle.vehicleBrand.label = Brand of the Vehicle thing-type.config.mybmw.vehicle.vin.description = Unique VIN given by BMW +thing-type.config.mybmw.vehicle.vin.label = Vehicle Identification Number (VIN) +thing-type.mybmw.account.description = Your BMW account data +thing-type.mybmw.account.label = MyBMW Account +thing-type.mybmw.bev_rex.description = Battery Electric Vehicle with Range Extender (BEV_REX) +thing-type.mybmw.bev_rex.label = Electric Vehicle with REX +thing-type.mybmw.bev.description = Battery Electric Vehicle (BEV) +thing-type.mybmw.bev.label = Electric Vehicle +thing-type.mybmw.conv.description = Conventional Fuel Vehicle (CONV) +thing-type.mybmw.conv.label = Conventional Vehicle +thing-type.mybmw.phev.description = Conventional Fuel Vehicle with supporting Electric Engine (PHEV) +thing-type.mybmw.phev.label = Plug-In-Hybrid Electric Vehicle # channel group types - -channel-group-type.mybmw.charge-statistic.label = Charging Statistics channel-group-type.mybmw.charge-statistic.description = Charging statistics of current month -channel-group-type.mybmw.check-control-values.label = Check Control Messages +channel-group-type.mybmw.charge-statistic.label = Charging Statistics channel-group-type.mybmw.check-control-values.description = Shows current active CheckControl messages -channel-group-type.mybmw.conv-range-values.label = Range and Fuel Data +channel-group-type.mybmw.check-control-values.label = Check Control Messages channel-group-type.mybmw.conv-range-values.description = Provides Mileage, remaining range and fuel level values -channel-group-type.mybmw.door-values.label = Detailed Door Status +channel-group-type.mybmw.conv-range-values.label = Range and Fuel Data channel-group-type.mybmw.door-values.description = Detailed Status of all Doors and Windows -channel-group-type.mybmw.ev-range-values.label = Range and Charge Data +channel-group-type.mybmw.door-values.label = Detailed Door Status channel-group-type.mybmw.ev-range-values.description = Provides Mileage, remaining range and charge level values -channel-group-type.mybmw.ev-vehicle-status.label = Vehicle Status +channel-group-type.mybmw.ev-range-values.label = Range and Charge Data channel-group-type.mybmw.ev-vehicle-status.description = Overall vehicle status -channel-group-type.mybmw.hybrid-range-values.label = Range, Charge / Fuel Data +channel-group-type.mybmw.ev-vehicle-status.label = Vehicle Status channel-group-type.mybmw.hybrid-range-values.description = Provides mileage, remaining fuel and range data for hybrid vehicles -channel-group-type.mybmw.image-values.label = Vehicle Image +channel-group-type.mybmw.hybrid-range-values.label = Range, Charge / Fuel Data channel-group-type.mybmw.image-values.description = Provides an image of your vehicle -channel-group-type.mybmw.location-values.label = Vehicle Location +channel-group-type.mybmw.image-values.label = Vehicle Image + channel-group-type.mybmw.location-values.description = Coordinates and heading of the vehicle -channel-group-type.mybmw.profile-values.label = Electric Charging Profile +channel-group-type.mybmw.location-values.label = Vehicle Location channel-group-type.mybmw.profile-values.description = Scheduled charging profiles -channel-group-type.mybmw.remote-services.label = Remote Services +channel-group-type.mybmw.profile-values.label = Electric Charging Profile channel-group-type.mybmw.remote-services.description = Remote control of the vehicle -channel-group-type.mybmw.service-values.label = Vehicle Services +channel-group-type.mybmw.remote-services.label = Remote Services channel-group-type.mybmw.service-values.description = Future vehicle service schedules -channel-group-type.mybmw.session-values.label = Electric Charging Sessions +channel-group-type.mybmw.service-values.label = Vehicle Services channel-group-type.mybmw.session-values.description = Past charging sessions -channel-group-type.mybmw.tire-pressures.label = Tire Pressure +channel-group-type.mybmw.session-values.label = Electric Charging Sessions channel-group-type.mybmw.tire-pressures.description = Current and wanted pressure for all tires -channel-group-type.mybmw.vehicle-status.label = Vehicle Status +channel-group-type.mybmw.tire-pressures.label = Tire Pressure channel-group-type.mybmw.vehicle-status.description = Overall vehicle status +channel-group-type.mybmw.vehicle-status.label = Vehicle Status # channel types - channel-type.mybmw.address-channel.label = Address channel-type.mybmw.charging-info-channel.label = Charging Information +channel-type.mybmw.charging-remaining-channel.label = Remaining Charging Time channel-type.mybmw.charging-status-channel.label = Charging Status channel-type.mybmw.check-control-channel.label = Check Control channel-type.mybmw.checkcontrol-details-channel.label = CheckControl Details channel-type.mybmw.checkcontrol-name-channel.label = CheckControl Description channel-type.mybmw.checkcontrol-severity-channel.label = Severity Level + channel-type.mybmw.doors-channel.label = Overall Door Status channel-type.mybmw.driver-front-channel.label = Driver Door channel-type.mybmw.driver-rear-channel.label = Driver Door Rear +channel-type.mybmw.estimated-fuel-l-100km-channel.label = Estimated consumption l/100km +channel-type.mybmw.estimated-fuel-mpg-channel.label = Estimated consumption mpg channel-type.mybmw.front-left-current-channel.label = Tire Pressure Front Left channel-type.mybmw.front-left-target-channel.label = Tire Pressure Front Left Target channel-type.mybmw.front-right-current-channel.label = Tire Pressure Front Right channel-type.mybmw.front-right-target-channel.label = Tire Pressure Front Right Target channel-type.mybmw.gps-channel.label = GPS Coordinates channel-type.mybmw.heading-channel.label = Heading Angle -channel-type.mybmw.home-distance-channel.label = Distance from Home channel-type.mybmw.home-distance-channel.description = Computed distance between vehicle and home location +channel-type.mybmw.home-distance-channel.label = Distance From Home channel-type.mybmw.hood-channel.label = Hood -channel-type.mybmw.image-view-channel.label = Image Viewport + +channel-type.mybmw.image-view-channel.command.option.FrontLeft = Left Side View +channel-type.mybmw.image-view-channel.command.option.FrontRight = Right Side View +channel-type.mybmw.image-view-channel.command.option.FrontView = Front View +channel-type.mybmw.image-view-channel.command.option.RearView = Rear View channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Front Side View -channel-type.mybmw.image-view-channel.command.option.VehicleInfo = Front View -channel-type.mybmw.image-view-channel.command.option.ChargingHistory = Side View -channel-type.mybmw.image-view-channel.command.option.Default = Default View -channel-type.mybmw.last-update-channel.label = Last Status Timestamp -channel-type.mybmw.last-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM +channel-type.mybmw.image-view-channel.label = Image Viewport +channel-type.mybmw.last-fetched-channel.label = Last Openhab Update Timestamp +channel-type.mybmw.last-update-channel.label = Last Car Status Timestamp channel-type.mybmw.lock-channel.label = Doors Locked channel-type.mybmw.mileage-channel.label = Total Distance Driven -channel-type.mybmw.motion-channel.label = Motion Status + channel-type.mybmw.next-service-date-channel.label = Next Service Date channel-type.mybmw.next-service-date-channel.state.pattern = %1$tb %1$tY channel-type.mybmw.next-service-mileage-channel.label = Mileage Till Next Service @@ -107,22 +108,24 @@ channel-type.mybmw.passenger-front-channel.label = Passenger Door channel-type.mybmw.passenger-rear-channel.label = Passenger Door Rear channel-type.mybmw.plug-connection-channel.label = Plug Connection Status channel-type.mybmw.png-channel.label = Rendered Vehicle Image + channel-type.mybmw.profile-climate-channel.label = A/C at Departure Time -channel-type.mybmw.profile-control-channel.label = Charging Plan -channel-type.mybmw.profile-control-channel.description = Charging plan selection channel-type.mybmw.profile-control-channel.command.option.weeklyPlanner = Weekly Schedule -channel-type.mybmw.profile-limit-channel.label = Charging Energy Limited +channel-type.mybmw.profile-control-channel.description = Charging plan selection +channel-type.mybmw.profile-control-channel.label = Charging Plan channel-type.mybmw.profile-limit-channel.description = Limited charging activated -channel-type.mybmw.profile-mode-channel.label = Charge Mode -channel-type.mybmw.profile-mode-channel.description = Mode for selecting immediate or delyed charging -channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Immediate Charging +channel-type.mybmw.profile-limit-channel.label = Charging Energy Limited channel-type.mybmw.profile-mode-channel.command.option.delayedCharging = Use Charging Preference -channel-type.mybmw.profile-prefs-channel.label = Charge Preferences -channel-type.mybmw.profile-prefs-channel.description = Preferences for delayed charging -channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = No Selection +channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Immediate Charging +channel-type.mybmw.profile-mode-channel.description = Mode for selecting immediate or delyed charging +channel-type.mybmw.profile-mode-channel.label = Charge Mode channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Charging Window -channel-type.mybmw.profile-target-channel.label = SOC Target +channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = No Selection +channel-type.mybmw.profile-prefs-channel.description = Preferences for delayed charging +channel-type.mybmw.profile-prefs-channel.label = Charge Preferences channel-type.mybmw.profile-target-channel.description = SOC charging target +channel-type.mybmw.profile-target-channel.label = SOC Target + channel-type.mybmw.range-electric-channel.label = Electric Range channel-type.mybmw.range-fuel-channel.label = Fuel Range channel-type.mybmw.range-hybrid-channel.label = Hybrid Range @@ -130,17 +133,25 @@ channel-type.mybmw.range-radius-electric-channel.label = Electric Range Radius channel-type.mybmw.range-radius-fuel-channel.label = Fuel Range Radius channel-type.mybmw.range-radius-hybrid-channel.label = Hybrid Range Radius channel-type.mybmw.raw-channel.label = Raw Data + channel-type.mybmw.rear-left-current-channel.label = Tire Pressure Rear Left channel-type.mybmw.rear-left-target-channel.label = Tire Pressure Rear Left Target channel-type.mybmw.rear-right-current-channel.label = Tire Pressure Rear Right channel-type.mybmw.rear-right-target-channel.label = Tire Pressure Rear Right Target channel-type.mybmw.remaining-fuel-channel.label = Remaining Fuel +channel-type.mybmw.remote-command-channel.command.option.climate-now-start = Start Climate now +channel-type.mybmw.remote-command-channel.command.option.climate-now-stop = Stop Climate now +channel-type.mybmw.remote-command-channel.command.option.door-lock = Lock vehicle +channel-type.mybmw.remote-command-channel.command.option.door-unlock = Unlock vehicle +channel-type.mybmw.remote-command-channel.command.option.horn-blow = Blow horn +channel-type.mybmw.remote-command-channel.command.option.light-flash = Flash lights +channel-type.mybmw.remote-command-channel.command.option.vehicle-finder = Find vehicle channel-type.mybmw.remote-command-channel.label = Remote Command channel-type.mybmw.remote-state-channel.label = Service Execution State channel-type.mybmw.service-date-channel.label = Service Date channel-type.mybmw.service-date-channel.state.pattern = %1$tb %1$tY channel-type.mybmw.service-details-channel.label = Service Details -channel-type.mybmw.service-mileage-channel.label = Mileage till Service +channel-type.mybmw.service-mileage-channel.label = Mileage until Service channel-type.mybmw.service-name-channel.label = Service Name channel-type.mybmw.session-energy-channel.label = Charged Energy in Session channel-type.mybmw.session-issue-channel.label = Issues during Session @@ -148,97 +159,99 @@ channel-type.mybmw.session-status-channel.label = Session Status channel-type.mybmw.session-subtitle-channel.label = Session Details channel-type.mybmw.session-title-channel.label = Session Title channel-type.mybmw.soc-channel.label = Battery Charge Level -channel-type.mybmw.statistic-energy-channel.label = Energy Charged + channel-type.mybmw.statistic-energy-channel.description = Total energy charged in current month -channel-type.mybmw.statistic-sessions-channel.label = Charge Sessions +channel-type.mybmw.statistic-energy-channel.label = Energy Charged channel-type.mybmw.statistic-sessions-channel.description = Number of charging sessions this month +channel-type.mybmw.statistic-sessions-channel.label = Charge Sessions channel-type.mybmw.statistic-title-channel.label = Charge Statistic Month channel-type.mybmw.sunroof-channel.label = Sunroof -channel-type.mybmw.timer1-day-fri-channel.label = T1 Friday + channel-type.mybmw.timer1-day-fri-channel.description = Friday scheduled for timer 1 -channel-type.mybmw.timer1-day-mon-channel.label = T1 Monday +channel-type.mybmw.timer1-day-fri-channel.label = T1 Friday channel-type.mybmw.timer1-day-mon-channel.description = Monday scheduled for timer 1 -channel-type.mybmw.timer1-day-sat-channel.label = T1 Saturday +channel-type.mybmw.timer1-day-mon-channel.label = T1 Monday channel-type.mybmw.timer1-day-sat-channel.description = Saturday scheduled for timer 1 -channel-type.mybmw.timer1-day-sun-channel.label = T1 Sunday +channel-type.mybmw.timer1-day-sat-channel.label = T1 Saturday channel-type.mybmw.timer1-day-sun-channel.description = Sunday scheduled for timer 1 -channel-type.mybmw.timer1-day-thu-channel.label = T1 Thursday +channel-type.mybmw.timer1-day-sun-channel.label = T1 Sunday channel-type.mybmw.timer1-day-thu-channel.description = Thursday scheduled for timer 1 -channel-type.mybmw.timer1-day-tue-channel.label = T1 Tuesday +channel-type.mybmw.timer1-day-thu-channel.label = T1 Thursday channel-type.mybmw.timer1-day-tue-channel.description = Tuesday scheduled for timer 1 -channel-type.mybmw.timer1-day-wed-channel.label = T1 Wednesday +channel-type.mybmw.timer1-day-tue-channel.label = T1 Tuesday channel-type.mybmw.timer1-day-wed-channel.description = Wednesday scheduled for timer 1 -channel-type.mybmw.timer1-departure-channel.label = T1 Departure Time +channel-type.mybmw.timer1-day-wed-channel.label = T1 Wednesday channel-type.mybmw.timer1-departure-channel.description = Departure time for regular schedule timer 1 +channel-type.mybmw.timer1-departure-channel.label = T1 Departure Time channel-type.mybmw.timer1-departure-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.timer1-enabled-channel.label = T1 Enabled channel-type.mybmw.timer1-enabled-channel.description = Timer 1 enabled -channel-type.mybmw.timer2-day-fri-channel.label = T2 Friday +channel-type.mybmw.timer1-enabled-channel.label = T1 Enabled channel-type.mybmw.timer2-day-fri-channel.description = Friday scheduled for timer 2 -channel-type.mybmw.timer2-day-mon-channel.label = T2 Monday +channel-type.mybmw.timer2-day-fri-channel.label = T2 Friday channel-type.mybmw.timer2-day-mon-channel.description = Monday scheduled for timer 2 -channel-type.mybmw.timer2-day-sat-channel.label = T2 Saturday +channel-type.mybmw.timer2-day-mon-channel.label = T2 Monday channel-type.mybmw.timer2-day-sat-channel.description = Saturday scheduled for timer 2 -channel-type.mybmw.timer2-day-sun-channel.label = T2 Sunday +channel-type.mybmw.timer2-day-sat-channel.label = T2 Saturday channel-type.mybmw.timer2-day-sun-channel.description = Sunday scheduled for timer 2 -channel-type.mybmw.timer2-day-thu-channel.label = T2 Thursday +channel-type.mybmw.timer2-day-sun-channel.label = T2 Sunday channel-type.mybmw.timer2-day-thu-channel.description = Thursday scheduled for timer 2 -channel-type.mybmw.timer2-day-tue-channel.label = T2 Tuesday +channel-type.mybmw.timer2-day-thu-channel.label = T2 Thursday channel-type.mybmw.timer2-day-tue-channel.description = Tuesday scheduled for timer 2 -channel-type.mybmw.timer2-day-wed-channel.label = T2 Wednesday +channel-type.mybmw.timer2-day-tue-channel.label = T2 Tuesday channel-type.mybmw.timer2-day-wed-channel.description = Wednesday scheduled for timer 2 -channel-type.mybmw.timer2-departure-channel.label = T2 Departure Time +channel-type.mybmw.timer2-day-wed-channel.label = T2 Wednesday channel-type.mybmw.timer2-departure-channel.description = Departure time for regular schedule timer 2 +channel-type.mybmw.timer2-departure-channel.label = T2 Departure Time channel-type.mybmw.timer2-departure-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.timer2-enabled-channel.label = T2 Enabled channel-type.mybmw.timer2-enabled-channel.description = Timer 2 enabled -channel-type.mybmw.timer3-day-fri-channel.label = T3 Friday +channel-type.mybmw.timer2-enabled-channel.label = T2 Enabled channel-type.mybmw.timer3-day-fri-channel.description = Friday scheduled for timer 3 -channel-type.mybmw.timer3-day-mon-channel.label = T3 Monday +channel-type.mybmw.timer3-day-fri-channel.label = T3 Friday channel-type.mybmw.timer3-day-mon-channel.description = Monday scheduled for timer 3 -channel-type.mybmw.timer3-day-sat-channel.label = T3 Saturday +channel-type.mybmw.timer3-day-mon-channel.label = T3 Monday channel-type.mybmw.timer3-day-sat-channel.description = Saturday scheduled for timer 3 -channel-type.mybmw.timer3-day-sun-channel.label = T3 Sunday +channel-type.mybmw.timer3-day-sat-channel.label = T3 Saturday channel-type.mybmw.timer3-day-sun-channel.description = Sunday scheduled for timer 3 -channel-type.mybmw.timer3-day-thu-channel.label = T3 Thursday +channel-type.mybmw.timer3-day-sun-channel.label = T3 Sunday channel-type.mybmw.timer3-day-thu-channel.description = Thursday scheduled for timer 3 -channel-type.mybmw.timer3-day-tue-channel.label = T3 Tuesday +channel-type.mybmw.timer3-day-thu-channel.label = T3 Thursday channel-type.mybmw.timer3-day-tue-channel.description = Tuesday scheduled for timer 3 -channel-type.mybmw.timer3-day-wed-channel.label = T3 Wednesday +channel-type.mybmw.timer3-day-tue-channel.label = T3 Tuesday channel-type.mybmw.timer3-day-wed-channel.description = Wednesday scheduled for timer 3 -channel-type.mybmw.timer3-departure-channel.label = T3 Departure Time +channel-type.mybmw.timer3-day-wed-channel.label = T3 Wednesday channel-type.mybmw.timer3-departure-channel.description = Departure time for regular schedule timer 3 +channel-type.mybmw.timer3-departure-channel.label = T3 Departure Time channel-type.mybmw.timer3-departure-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.timer3-enabled-channel.label = T3 Enabled channel-type.mybmw.timer3-enabled-channel.description = Timer 3 enabled -channel-type.mybmw.timer4-day-fri-channel.label = T4 Friday +channel-type.mybmw.timer3-enabled-channel.label = T3 Enabled channel-type.mybmw.timer4-day-fri-channel.description = Friday scheduled for timer 4 -channel-type.mybmw.timer4-day-mon-channel.label = T4 Monday +channel-type.mybmw.timer4-day-fri-channel.label = T4 Friday channel-type.mybmw.timer4-day-mon-channel.description = Monday scheduled for timer 4 -channel-type.mybmw.timer4-day-sat-channel.label = T4 Saturday +channel-type.mybmw.timer4-day-mon-channel.label = T4 Monday channel-type.mybmw.timer4-day-sat-channel.description = Saturday scheduled for timer 4 -channel-type.mybmw.timer4-day-sun-channel.label = T4 Sunday +channel-type.mybmw.timer4-day-sat-channel.label = T4 Saturday channel-type.mybmw.timer4-day-sun-channel.description = Sunday scheduled for timer 4 -channel-type.mybmw.timer4-day-thu-channel.label = T4 Thursday +channel-type.mybmw.timer4-day-sun-channel.label = T4 Sunday channel-type.mybmw.timer4-day-thu-channel.description = Thursday scheduled for timer 4 -channel-type.mybmw.timer4-day-tue-channel.label = T4 Tuesday +channel-type.mybmw.timer4-day-thu-channel.label = T4 Thursday channel-type.mybmw.timer4-day-tue-channel.description = Tuesday scheduled for timer 4 -channel-type.mybmw.timer4-day-wed-channel.label = T4 Wednesday +channel-type.mybmw.timer4-day-tue-channel.label = T4 Tuesday channel-type.mybmw.timer4-day-wed-channel.description = Wednesday scheduled for timer 4 -channel-type.mybmw.timer4-departure-channel.label = T4 Departure Time +channel-type.mybmw.timer4-day-wed-channel.label = T4 Wednesday channel-type.mybmw.timer4-departure-channel.description = Departure time for regular schedule timer 4 +channel-type.mybmw.timer4-departure-channel.label = T4 Departure Time channel-type.mybmw.timer4-departure-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.timer4-enabled-channel.label = T4 Enabled channel-type.mybmw.timer4-enabled-channel.description = Timer 4 enabled +channel-type.mybmw.timer4-enabled-channel.label = T4 Enabled channel-type.mybmw.trunk-channel.label = Trunk channel-type.mybmw.window-driver-front-channel.label = Driver Window channel-type.mybmw.window-driver-rear-channel.label = Driver Rear Window -channel-type.mybmw.window-end-channel.label = Window End Time channel-type.mybmw.window-end-channel.description = End time of charging window +channel-type.mybmw.window-end-channel.label = Window End Time channel-type.mybmw.window-end-channel.state.pattern = %1$tH:%1$tM channel-type.mybmw.window-passenger-front-channel.label = Passenger Window channel-type.mybmw.window-passenger-rear-channel.label = Passenger Rear Window -channel-type.mybmw.window-start-channel.label = Window Start Time channel-type.mybmw.window-start-channel.description = Start time of charging window +channel-type.mybmw.window-start-channel.label = Window Start Time channel-type.mybmw.window-start-channel.state.pattern = %1$tH:%1$tM channel-type.mybmw.windows-channel.label = Overall Window Status diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties index 060bada394914..2efe5538461dd 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties @@ -1,225 +1,257 @@ -# add-on -addon.mybmw.name = MyBMW -addon.mybmw.description = Fahrzeugdaten über die MyBMW App +# Binding +binding.mybmw.name = MyBMW +binding.mybmw.description = Fahrzeugdaten über die MyBMW App -# bridge types -thing-type.mybmw.account.label = MyBMW Benutzerkonto -thing-type.mybmw.account.description = Kontodaten für das BMW Benutzerkonto - -# bridge config -thing-type.config.mybmw.bridge.userName.label = Benutzername -thing-type.config.mybmw.bridge.userName.description = Benutzername für die MyBMW App +# thing types +thing-type.config.mybmw.bridge.language.description = Daten werden für die gewünschte Sprache angefordert (en, de, fr ...) +thing-type.config.mybmw.bridge.language.label = Sprachauswahl +thing-type.config.mybmw.bridge.password.description = Passwort für die MyBMW App thing-type.config.mybmw.bridge.password.label = Passwort -thing-type.config.mybmw.bridge.password.description = Passwort für die MyBMW App -thing-type.config.mybmw.bridge.region.label = Region thing-type.config.mybmw.bridge.region.description = Auswahl Ihrer Region -thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = Nordamerika +thing-type.config.mybmw.bridge.region.label = Region thing-type.config.mybmw.bridge.region.option.CHINA = China +thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = Nordamerika thing-type.config.mybmw.bridge.region.option.ROW = Rest der Welt -thing-type.config.mybmw.bridge.language.label = Sprachauswahl -thing-type.config.mybmw.bridge.language.description = Daten werden für die gewünschte Sprache angefordert (en, de, fr ...) +thing-type.config.mybmw.bridge.userName.description = Benutzername für die MyBMW App +thing-type.config.mybmw.bridge.userName.label = Benutzername -# thing types -thing-type.mybmw.bev_rex.label = Elektrofahrzeug mit REX +thing-type.config.mybmw.vehicle.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs +thing-type.config.mybmw.vehicle.refreshInterval.label = Datenaktualisierung in Minuten +thing-type.config.mybmw.vehicle.vehicleBrand.description = Fahrzeugmarke wie z.B. BMW oder Mini. +thing-type.config.mybmw.vehicle.vehicleBrand.label = Marke des Fahrzeugs +thing-type.config.mybmw.vehicle.vin.description = VIN des Fahrzeugs +thing-type.config.mybmw.vehicle.vin.label = Fahrzeug Identifikationsnummer (VIN) +thing-type.mybmw.account.description = Kontodaten für das BMW Benutzerkonto +thing-type.mybmw.account.label = MyBMW Benutzerkonto thing-type.mybmw.bev_rex.description = Elektrofahrzeug mit Range Extender (bev_rex) -thing-type.mybmw.bev.label = Elektrofahrzeug +thing-type.mybmw.bev_rex.label = Elektrofahrzeug mit REX thing-type.mybmw.bev.description = Batterieelektrisches Fahrzeug (bev) -thing-type.mybmw.phev.label = Plug-in-Hybrid Elektrofahrzeug -thing-type.mybmw.phev.description = Konventionelles Fahrzeug mit Elektromotor (phev) -thing-type.mybmw.conv.label = Konventionelles Fahrzeug +thing-type.mybmw.bev.label = Elektrofahrzeug thing-type.mybmw.conv.description = Konventionelles Benzin/Diesel Fahrzeug (conv) - -# thing config -thing-type.config.mybmw.vehicle.vin.label = Fahrzeug Identifikationsnummer (VIN) -thing-type.config.mybmw.vehicle.vin.description = VIN des Fahrzeugs -thing-type.config.mybmw.vehicle.refreshInterval.label = Datenaktualisierung in Minuten -thing-type.config.mybmw.vehicle.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs -thing-type.config.mybmw.vehicle.vehicleBrand.label = Marke des Fahrzeugs -thing-type.config.mybmw.vehicle.vehicleBrand.description = Fahrzeugmarke wie z.B. BMW oder Mini. +thing-type.mybmw.conv.label = Konventionelles Fahrzeug +thing-type.mybmw.phev.description = Konventionelles Fahrzeug mit Elektromotor (phev) +thing-type.mybmw.phev.label = Plug-in-Hybrid Elektrofahrzeug # Channel Groups -channel-group-type.mybmw.ev-vehicle-status.label = Fahrzeug Zustand -channel-group-type.mybmw.ev-vehicle-status.description = Gesamtzustand des Fahrzeugs -channel-group-type.mybmw.vehicle-status.label = Fahrzeug Zustand -channel-group-type.mybmw.vehicle-status.description = Gesamtzustand des Fahrzeugs -channel-group-type.mybmw.ev-range-values.label = Elektro- Reichweiten und Batterieladung -channel-group-type.mybmw.ev-range-values.description = Tachostand, Reichweiten und Ladestand des Fahrzeugs -channel-group-type.mybmw.hybrid-range-values.label = Hybride Reichweiten und Füllstände -channel-group-type.mybmw.hybrid-range-values.description = Tachostand, Reichweite, Ladezustand und Tankfüllung für hybride Fahrzeuge -channel-group-type.mybmw.conv-range-values.label = Verbrenner Reichweiten und Füllstände -channel-group-type.mybmw.conv-range-values.description = Tachostand, Reichweite und Tankfüllung des Fahrzeugs -channel-group-type.mybmw.door-values.label = Details aller Türen -channel-group-type.mybmw.door-values.description = Zeigt die Details der Türen und Fenster des Fahrzeugs -channel-group-type.mybmw.check-control-values.label = Warnungen +channel-group-type.mybmw.charge-statistic.description = Statistik der Ladevorgänge im Monat +channel-group-type.mybmw.charge-statistic.label = Elektrische Ladestatistik channel-group-type.mybmw.check-control-values.description = Aktuelle Warnungen des Fahrzeugs -channel-group-type.mybmw.service-values.label = Wartung -channel-group-type.mybmw.service-values.description = Anstehende Wartungstermine des Fahrzeugs -channel-group-type.mybmw.location-values.label = Fahrzeug Standort +channel-group-type.mybmw.check-control-values.label = Warnungen +channel-group-type.mybmw.conv-range-values.description = Tachostand, Reichweite und Tankfüllung des Fahrzeugs +channel-group-type.mybmw.conv-range-values.label = Verbrenner Reichweiten und Füllstände +channel-group-type.mybmw.door-values.description = Zeigt die Details der Türen und Fenster des Fahrzeugs +channel-group-type.mybmw.door-values.label = Details aller Türen +channel-group-type.mybmw.ev-range-values.description = Tachostand, Reichweiten und Ladestand des Fahrzeugs +channel-group-type.mybmw.ev-range-values.label = Elektro- Reichweiten und Batterieladung +channel-group-type.mybmw.ev-vehicle-status.description = Gesamtzustand des Fahrzeugs +channel-group-type.mybmw.ev-vehicle-status.label = Fahrzeug Zustand +channel-group-type.mybmw.hybrid-range-values.description = Tachostand, Reichweite, Ladezustand und Tankfüllung für hybride Fahrzeuge +channel-group-type.mybmw.hybrid-range-values.label = Hybride Reichweiten und Füllstände +channel-group-type.mybmw.image-values.description = Bild des Fahrzeug basierend auf der ausgewählten Ansicht +channel-group-type.mybmw.image-values.label = Fahrzeug Bild + channel-group-type.mybmw.location-values.description = Koordinaten und Ausrichtung des Fahrzeugs -channel-group-type.mybmw.remote-services.label = Fernsteuerung -channel-group-type.mybmw.remote-services.description = Fernsteuerung des Fahrzeugs +channel-group-type.mybmw.location-values.label = Fahrzeug Standort +channel-group-type.mybmw.profile-values.description = Zeitplanung der Ladevorgänge channel-group-type.mybmw.profile-values.label = Elektrisches Ladeprofil -channel-group-type.mybmw.profile-values.description = Zeitplanung der Ladevorgänge -channel-group-type.mybmw.charge-statistic.label = Elektrische Ladestatistik -channel-group-type.mybmw.charge-statistic.description = Statistik der Ladevorgänge im Monat -channel-group-type.mybmw.session-values.label = Elektrische Ladevorgänge -channel-group-type.mybmw.session-values.description = Liste der letzten Ladevorgänge -channel-group-type.mybmw.tire-pressures.label = Reifen Luftdruck +channel-group-type.mybmw.remote-services.description = Fernsteuerung des Fahrzeugs +channel-group-type.mybmw.remote-services.label = Fernsteuerung +channel-group-type.mybmw.service-values.description = Anstehende Wartungstermine des Fahrzeugs +channel-group-type.mybmw.service-values.label = Wartung +channel-group-type.mybmw.session-values.description = Liste der letzten Ladevorgänge +channel-group-type.mybmw.session-values.label = Elektrische Ladevorgänge channel-group-type.mybmw.tire-pressures.description = Reifen Luftdruck Ist und Sollwerte -channel-group-type.mybmw.image-values.label = Fahrzeug Bild -channel-group-type.mybmw.image-values.description = Bild des Fahrzeug basierend auf der ausgewählten Ansicht - - +channel-group-type.mybmw.tire-pressures.label = Reifen Luftdruck +channel-group-type.mybmw.vehicle-status.description = Gesamtzustand des Fahrzeugs +channel-group-type.mybmw.vehicle-status.label = Fahrzeug Zustand # Channel Types -channel-type.mybmw.doors-channel.label = Gesamtzustand der Türen -channel-type.mybmw.windows-channel.label = Gesamtzustand der Fenster -channel-type.mybmw.lock-channel.label = Fahrzeug Abgeschlossen -channel-type.mybmw.next-service-date-channel.label = Nächster Service Termin -channel-type.mybmw.next-service-mileage-channel.label = Nächster Service in Kilometern -channel-type.mybmw.check-control-channel.label = Warnung Aktiv -channel-type.mybmw.plug-connection-channel.label = Ladestecker -channel-type.mybmw.charging-status-channel.label = Ladezustand +channel-type.mybmw.address-channel.label = Adresse channel-type.mybmw.charging-info-channel.label = Ladeinformationen -channel-type.mybmw.motion-channel.label = Fahrzustand -channel-type.mybmw.last-update-channel.label = Letzte Aktualisierung -channel-type.mybmw.raw-channel.label = Rohdaten +channel-type.mybmw.charging-remaining-channel.label = Verbleibende Ladezeit +channel-type.mybmw.charging-status-channel.label = Ladezustand +channel-type.mybmw.check-control-channel.label = Warnung Aktiv +channel-type.mybmw.checkcontrol-details-channel.label = Warnung Details +channel-type.mybmw.checkcontrol-name-channel.label = Warnung +channel-type.mybmw.checkcontrol-severity-channel.label = Warnung Priorität -channel-type.mybmw.driver-front-channel.label = Fahrertür -channel-type.mybmw.driver-rear-channel.label = Fahrertür Hinten -channel-type.mybmw.passenger-front-channel.label = Beifahrertür -channel-type.mybmw.passenger-rear-channel.label = Beifahrertür Hinten +channel-type.mybmw.doors-channel.label = Gesamtzustand der Türen +channel-type.mybmw.driver-front-channel.label = Fahrertür +channel-type.mybmw.driver-rear-channel.label = Fahrertür Hinten +channel-type.mybmw.estimated-fuel-l-100km-channel.label = Geschätzter Verbrauch l/100km +channel-type.mybmw.estimated-fuel-mpg-channel.label = Geschätzter Verbrauch mpg +channel-type.mybmw.front-left-current-channel.label = Reifen Luftdruck Vorne Links +channel-type.mybmw.front-left-target-channel.label = Reifen Luftdruck Vorne Links Sollwert +channel-type.mybmw.front-right-current-channel.label = Reifen Luftdruck Vorne Rechts +channel-type.mybmw.front-right-target-channel.label = Reifen Luftdruck Vorne Rechts Sollwert +channel-type.mybmw.gps-channel.label = Koordinaten +channel-type.mybmw.heading-channel.label = Ausrichtung +channel-type.mybmw.home-distance-channel.description = Berechnete Entfernung zwischen Fahrzeug und Heimatort +channel-type.mybmw.home-distance-channel.label = Entfernung vom Heimatort channel-type.mybmw.hood-channel.label = Frontklappe -channel-type.mybmw.trunk-channel.label = Heckklappe -channel-type.mybmw.window-driver-front-channel.label = Fahrertür Fenster -channel-type.mybmw.window-driver-rear-channel.label = Fahrertür Hinten Fenster -channel-type.mybmw.window-passenger-front-channel.label = Beifahrertür Fenster -channel-type.mybmw.window-passenger-rear-channel.label = Beifahrertür Hinten Fenster -channel-type.mybmw.window-rear-channel.label = Heckfenster -channel-type.mybmw.sunroof-channel.label = Schiebedach +channel-type.mybmw.image-view-channel.command.option.FrontLeft = Seitenansicht links +channel-type.mybmw.image-view-channel.command.option.FrontRight = Seitenansicht rechts +channel-type.mybmw.image-view-channel.command.option.FrontView = Frontansicht +channel-type.mybmw.image-view-channel.command.option.RearView = Heckansicht +channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Front Seitenansicht +channel-type.mybmw.image-view-channel.label = Fahrzeug Ansicht +channel-type.mybmw.last-fetched-channel.label = Letzte Aktualisierung in Openhab +channel-type.mybmw.last-update-channel.label = Letzte Aktualisierung des Autos +channel-type.mybmw.lock-channel.label = Fahrzeug Abgeschlossen channel-type.mybmw.mileage-channel.label = Tachostand -channel-type.mybmw.range-hybrid-channel.label = Hybride Reichweite + +channel-type.mybmw.next-service-date-channel.label = Nächster Service Termin +channel-type.mybmw.next-service-date-channel.state.pattern = %1$tb %1$tY +channel-type.mybmw.next-service-mileage-channel.label = Nächster Service in Kilometern +channel-type.mybmw.passenger-front-channel.label = Beifahrertür +channel-type.mybmw.passenger-rear-channel.label = Beifahrertür Hinten +channel-type.mybmw.plug-connection-channel.label = Ladestecker +channel-type.mybmw.png-channel.label = Fahrzeug Bild + +channel-type.mybmw.profile-climate-channel.label = Klimatisierung bei Abfahrt +channel-type.mybmw.profile-control-channel.command.option.weeklyPlanner = Wöchentliche Planung +channel-type.mybmw.profile-control-channel.description = Ladeplan Auswahl +channel-type.mybmw.profile-control-channel.label = Ladeplan +channel-type.mybmw.profile-limit-channel.description = Limitierte Ladung aktiviert +channel-type.mybmw.profile-limit-channel.label = Ladung Limitiert +channel-type.mybmw.profile-mode-channel.command.option.delayedCharging = Ladeverzögerung +channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Sofort Laden +channel-type.mybmw.profile-mode-channel.description = Lademoduswahl +channel-type.mybmw.profile-mode-channel.label = Ladeprofil +channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Laden im Zeitfenster +channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Keine Präferenz +channel-type.mybmw.profile-prefs-channel.description = Präferenz für Laden +channel-type.mybmw.profile-prefs-channel.label = Ladeprofil Präferenz +channel-type.mybmw.profile-target-channel.description = Erwünschter Batterie Ladezustand +channel-type.mybmw.profile-target-channel.label = Ziel Ladezustand + channel-type.mybmw.range-electric-channel.label = Elektrische Reichweite -channel-type.mybmw.soc-channel.label = Batterie Ladestand channel-type.mybmw.range-fuel-channel.label = Verbrenner Reichweite -channel-type.mybmw.remaining-fuel-channel.label = Tankstand +channel-type.mybmw.range-hybrid-channel.label = Hybride Reichweite channel-type.mybmw.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius channel-type.mybmw.range-radius-fuel-channel.label = Verbrenner Reichweiten-Radius channel-type.mybmw.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius +channel-type.mybmw.raw-channel.label = Rohdaten -channel-type.mybmw.service-name-channel.label = Service +channel-type.mybmw.rear-left-current-channel.label = Reifen Luftdruck Hinten Links +channel-type.mybmw.rear-left-target-channel.label = Reifen Luftdruck Hinten Links Sollwert +channel-type.mybmw.rear-right-current-channel.label = Reifen Luftdruck Hinten Rechts +channel-type.mybmw.rear-right-target-channel.label = Reifen Luftdruck Hinten Rechts Sollwert +channel-type.mybmw.remaining-fuel-channel.label = Tankstand +channel-type.mybmw.remote-command-channel.command.option.climate-now-start = Klimatisierung Ausführen +channel-type.mybmw.remote-command-channel.command.option.climate-now-stop = Klimatisierung Beenden +channel-type.mybmw.remote-command-channel.command.option.door-lock = Fahrzeug Abschließen +channel-type.mybmw.remote-command-channel.command.option.door-unlock = Fahrzug Aufschließen +channel-type.mybmw.remote-command-channel.command.option.horn-blow = Hupe Aktivieren +channel-type.mybmw.remote-command-channel.command.option.light-flash = Lichthupe Ausführen +channel-type.mybmw.remote-command-channel.command.option.vehicle-finder = Fahrzeug Lokalisieren +channel-type.mybmw.remote-command-channel.label = Kommando Auswahl +channel-type.mybmw.remote-state-channel.label = Ausführungszustand +channel-type.mybmw.service-date-channel.label = Service Datum +channel-type.mybmw.service-date-channel.state.pattern = %1$tb %1$tY channel-type.mybmw.service-details-channel.label = Service Details -channel-type.mybmw.service-date-channel.label = Service Termin channel-type.mybmw.service-mileage-channel.label = Service in Kilometern +channel-type.mybmw.service-name-channel.label = Service +channel-type.mybmw.session-energy-channel.label = Energie Geladen +channel-type.mybmw.session-issue-channel.label = Ladevorgang Probleme +channel-type.mybmw.session-status-channel.label = Ladevorgang Zustand +channel-type.mybmw.session-subtitle-channel.label = Ladevorgang Details +channel-type.mybmw.session-title-channel.label = Ladevorgang Beschreibung +channel-type.mybmw.soc-channel.label = Batterie Ladestand -channel-type.mybmw.checkcontrol-name-channel.label = Warnung -channel-type.mybmw.checkcontrol-details-channel.label = Warnung Details -channel-type.mybmw.checkcontrol-severity-channel.label = Warnung Priorität - -channel-type.mybmw.profile-climate-channel.label = Klimatisierung bei Abfahrt -channel-type.mybmw.profile-mode-channel.label = Ladeprofil -channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Sofort Laden -channel-type.mybmw.profile-mode-channel.command.option.delayedCharging = Ladeverzögerung -channel-type.mybmw.profile-prefs-channel.label = Ladeprofil Präferenz -channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Keine Präferenz -channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Laden im Zeitfenster -channel-type.mybmw.profile-control-channel.label = Ladeplan -channel-type.mybmw.profile-control-channel.description = Ladeplan Auswahl -channel-type.mybmw.profile-target-channel.label = Ziel Ladezustand -channel-type.mybmw.profile-target-channel.description = Erwünschter Batterie Ladezustand -channel-type.mybmw.profile-limit-channel.label = Ladung Limitiert -channel-type.mybmw.profile-limit-channell.description = Limitierte Ladung aktiviert - +channel-type.mybmw.statistic-energy-channel.description = Geladene Energie in diesem Monat +channel-type.mybmw.statistic-energy-channel.label = Energie Geladen Monat +channel-type.mybmw.statistic-sessions-channel.description = Anzahl der Ladevorgänge in diesem Monat +channel-type.mybmw.statistic-sessions-channel.label = Ladevorgänge Monat +channel-type.mybmw.statistic-title-channel.label = Ladestatistik Monat +channel-type.mybmw.sunroof-channel.label = Schiebedach -channel-type.mybmw.window-start-channel.label = Ladefenster Startzeit -channel-type.mybmw.window-end-channel.label = Ladefenster Endzeit -channel-type.mybmw.timer1-enabled-channel.label = Zeitprofil 1 - Aktiviert -channel-type.mybmw.timer1-departure-channel.label = Zeitprofil 1 - Abfahrtszeit -channel-type.mybmw.timer1-days-channel.label = Zeitprofil 1 - Tage -channel-type.mybmw.timer1-day-mon-channel.label = Zeitprofil 1 - Montag -channel-type.mybmw.timer1-day-tue-channel.label = Zeitprofil 1 - Dienstag -channel-type.mybmw.timer1-day-wed-channel.label = Zeitprofil 1 - Mittwoch -channel-type.mybmw.timer1-day-thu-channel.label = Zeitprofil 1 - Donnerstag +channel-type.mybmw.timer1-day-fri-channel.description = Freitag geplant für Zeitprofil 1 channel-type.mybmw.timer1-day-fri-channel.label = Zeitprofil 1 - Freitag +channel-type.mybmw.timer1-day-mon-channel.description = Montag geplant für Zeitprofil 1 +channel-type.mybmw.timer1-day-mon-channel.label = Zeitprofil 1 - Montag +channel-type.mybmw.timer1-day-sat-channel.description = Samstag geplant für Zeitprofil 1 channel-type.mybmw.timer1-day-sat-channel.label = Zeitprofil 1 - Samstag +channel-type.mybmw.timer1-day-sun-channel.description = Sonntag geplant für Zeitprofil 1 channel-type.mybmw.timer1-day-sun-channel.label = Zeitprofil 1 - Sonntag -channel-type.mybmw.timer2-enabled-channel.label = Zeitprofil 2 - Aktiviert -channel-type.mybmw.timer2-departure-channel.label = Zeitprofil 2 - Abfahrtszeit -channel-type.mybmw.timer2-days-channel.label = Zeitprofil 2 - Tage -channel-type.mybmw.timer2-day-mon-channel.label = Zeitprofil 2 - Montag -channel-type.mybmw.timer2-day-tue-channel.label = Zeitprofil 2 - Dienstag -channel-type.mybmw.timer2-day-wed-channel.label = Zeitprofil 2 - Mittwoch -channel-type.mybmw.timer2-day-thu-channel.label = Zeitprofil 2 - Donnerstag +channel-type.mybmw.timer1-day-thu-channel.description = Donnerstag geplant für Zeitprofil 1 +channel-type.mybmw.timer1-day-thu-channel.label = Zeitprofil 1 - Donnerstag +channel-type.mybmw.timer1-day-tue-channel.description = Dienstag geplant für Zeitprofil 1 +channel-type.mybmw.timer1-day-tue-channel.label = Zeitprofil 1 - Dienstag +channel-type.mybmw.timer1-day-wed-channel.description = Mittwoch geplant für Zeitprofil 1 +channel-type.mybmw.timer1-day-wed-channel.label = Zeitprofil 1 - Mittwoch +channel-type.mybmw.timer1-departure-channel.description = Abfahrtszeit für Zeitprofil 1 +channel-type.mybmw.timer1-departure-channel.label = Zeitprofil 1 - Abfahrtszeit +channel-type.mybmw.timer1-departure-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.timer1-enabled-channel.description = Timer 1 aktiviert +channel-type.mybmw.timer1-enabled-channel.label = Zeitprofil 1 - Aktiviert +channel-type.mybmw.timer2-day-fri-channel.description = Freitag geplant für Zeitprofil 2 channel-type.mybmw.timer2-day-fri-channel.label = Zeitprofil 2 - Freitag +channel-type.mybmw.timer2-day-mon-channel.description = Montag geplant für Zeitprofil 2 +channel-type.mybmw.timer2-day-mon-channel.label = Zeitprofil 2 - Montag +channel-type.mybmw.timer2-day-sat-channel.description = Samstag geplant für Zeitprofil 2 channel-type.mybmw.timer2-day-sat-channel.label = Zeitprofil 2 - Samstag +channel-type.mybmw.timer2-day-sun-channel.description = Sonntag geplant für Zeitprofil 2 channel-type.mybmw.timer2-day-sun-channel.label = Zeitprofil 2 - Sonntag -channel-type.mybmw.timer3-enabled-channel.label = Zeitprofil 3 - Aktiviert -channel-type.mybmw.timer3-departure-channel.label = Zeitprofil 3 - Abfahrtszeit -channel-type.mybmw.timer3-days-channel.label = Zeitprofil 3 - Tage -channel-type.mybmw.timer3-day-mon-channel.label = Zeitprofil 3 - Montag -channel-type.mybmw.timer3-day-tue-channel.label = Zeitprofil 3 - Dienstag -channel-type.mybmw.timer3-day-wed-channel.label = Zeitprofil 3 - Mittwoch -channel-type.mybmw.timer3-day-thu-channel.label = Zeitprofil 3 - Donnerstag +channel-type.mybmw.timer2-day-thu-channel.description = Donnerstag geplant für Zeitprofil 2 +channel-type.mybmw.timer2-day-thu-channel.label = Zeitprofil 2 - Donnerstag +channel-type.mybmw.timer2-day-tue-channel.description = Dienstag geplant für Zeitprofil 2 +channel-type.mybmw.timer2-day-tue-channel.label = Zeitprofil 2 - Dienstag +channel-type.mybmw.timer2-day-wed-channel.description = Mittwoch geplant für Zeitprofil 2 +channel-type.mybmw.timer2-day-wed-channel.label = Zeitprofil 2 - Mittwoch +channel-type.mybmw.timer2-departure-channel.description = Abfahrtszeit für Zeitprofil 2 +channel-type.mybmw.timer2-departure-channel.label = Zeitprofil 2 - Abfahrtszeit +channel-type.mybmw.timer2-departure-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.timer2-enabled-channel.description = Timer 2 aktiviert +channel-type.mybmw.timer2-enabled-channel.label = Zeitprofil 2 - Aktiviert +channel-type.mybmw.timer3-day-fri-channel.description = Freitag geplant für Zeitprofil 3 channel-type.mybmw.timer3-day-fri-channel.label = Zeitprofil 3 - Freitag +channel-type.mybmw.timer3-day-mon-channel.description = Montag geplant für Zeitprofil 3 +channel-type.mybmw.timer3-day-mon-channel.label = Zeitprofil 3 - Montag +channel-type.mybmw.timer3-day-sat-channel.description = Samstag geplant für Zeitprofil 3 channel-type.mybmw.timer3-day-sat-channel.label = Zeitprofil 3 - Samstag +channel-type.mybmw.timer3-day-sun-channel.description = Sonntag geplant für Zeitprofil 3 channel-type.mybmw.timer3-day-sun-channel.label = Zeitprofil 3 - Sonntag -channel-type.mybmw.timer4-enabled-channel.label = Zeitprofil 4 - Aktiviert -channel-type.mybmw.timer4-departure-channel.label = Zeitprofil 4 - Abfahrtszeit -channel-type.mybmw.timer4-days-channel.label = Zeitprofil 4 - Tage -channel-type.mybmw.timer4-day-mon-channel.label = Zeitprofil 4 - Montag -channel-type.mybmw.timer4-day-tue-channel.label = Zeitprofil 4 - Dienstag -channel-type.mybmw.timer4-day-wed-channel.label = Zeitprofil 4 - Mittwoch -channel-type.mybmw.timer4-day-thu-channel.label = Zeitprofil 4 - Donnerstag +channel-type.mybmw.timer3-day-thu-channel.description = Donnerstag geplant für Zeitprofil 3 +channel-type.mybmw.timer3-day-thu-channel.label = Zeitprofil 3 - Donnerstag +channel-type.mybmw.timer3-day-tue-channel.description = Dienstag geplant für Zeitprofil 3 +channel-type.mybmw.timer3-day-tue-channel.label = Zeitprofil 3 - Dienstag +channel-type.mybmw.timer3-day-wed-channel.description = Mittwoch geplant für Zeitprofil 3 +channel-type.mybmw.timer3-day-wed-channel.label = Zeitprofil 3 - Mittwoch +channel-type.mybmw.timer3-departure-channel.description = Abfahrtszeit für Zeitprofil 3 +channel-type.mybmw.timer3-departure-channel.label = Zeitprofil 3 - Abfahrtszeit +channel-type.mybmw.timer3-departure-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.timer3-enabled-channel.description = Timer 3 aktiviert +channel-type.mybmw.timer3-enabled-channel.label = Zeitprofil 3 - Aktiviert +channel-type.mybmw.timer4-day-fri-channel.description = Freitag geplant für Zeitprofil 4 channel-type.mybmw.timer4-day-fri-channel.label = Zeitprofil 4 - Freitag +channel-type.mybmw.timer4-day-mon-channel.description = Montag geplant für Zeitprofil 4 +channel-type.mybmw.timer4-day-mon-channel.label = Zeitprofil 4 - Montag +channel-type.mybmw.timer4-day-sat-channel.description = Samstag geplant für Zeitprofil 4 channel-type.mybmw.timer4-day-sat-channel.label = Zeitprofil 4 - Samstag +channel-type.mybmw.timer4-day-sun-channel.description = Sonntag geplant für Zeitprofil 4 channel-type.mybmw.timer4-day-sun-channel.label = Zeitprofil 4 - Sonntag - -# Location -channel-type.mybmw.gps-channel.label = Koordinaten -channel-type.mybmw.heading-channel.label = Ausrichtung -channel-type.mybmw.address-channel.label = Adresse - -#Remote -channel-type.mybmw.remote-command-channel.label = Kommando Auswahl -channel-type.mybmw.remote-command-channel.command.option.light-flash = Lichthupe Ausführen -channel-type.mybmw.remote-command-channel.command.option.vehicle-finder = Fahrzeug Lokalisieren -channel-type.mybmw.remote-command-channel.command.option.door-lock = Fahrzeug Abschließen -channel-type.mybmw.remote-command-channel.command.option.door-unlock = Fahrzug Aufschließen -channel-type.mybmw.remote-command-channel.command.option.horn-blow = Hupe Aktivieren -channel-type.mybmw.remote-command-channel.command.option.climate-now-start = Klimatisierung Ausführen -channel-type.mybmw.remote-command-channel.command.option.climate-now-stop = Klimatisierung Beenden -channel-type.mybmw.remote-state-channel.label = Ausführungszustand - -# Image -channel-type.mybmw.png-channel.label = Fahrzeug Bild -channel-type.mybmw.image-view-channel.label = Fahrzeug Ansicht -channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Front Seitenansicht -channel-type.mybmw.image-view-channel.command.option.VehicleInfo = Frontansicht -channel-type.mybmw.image-view-channel.command.option.ChargingHistory = Seitenansicht -channel-type.mybmw.image-view-channel.command.option.Default = Standard Ansicht - -# Charge Sessions -channel-type.mybmw.session-title-channel.label = Ladevorgang Beschreibung -channel-type.mybmw.session-subtitle-channel.label = Ladevorgang Details -channel-type.mybmw.session-energy-channel.label = Energie Geladen -channel-type.mybmw.session-issue-channel.label = Ladevorgang Probleme -channel-type.mybmw.session-status-channel.label = Ladevorgang Zustand - -# Charge Statistcis -channel-type.mybmw.statistic-title-channel.label = Ladestatistik Monat -channel-type.mybmw.statistic-energy-channel.label = Energie Geladen Monat -channel-type.mybmw.statistic-energy-channel.description = Geladene Energie in diesem Monat -channel-type.mybmw.statistic-sessions-channel.label = Ladevorgänge Monat -channel-type.mybmw.statistic-sessions-channel.description = Anzahl der Ladevorgänge in diesem Monat - -#Tires -channel-type.mybmw.front-left-current-channel.label = Reifen Luftdruck Vorne Links -channel-type.mybmw.front-left-wanted-channel.label = Reifen Luftdruck Vorne Links Sollwert -channel-type.mybmw.front-right-current-channel.label = Reifen Luftdruck Vorne Rechts -channel-type.mybmw.front-right-wanted-channel.label = Reifen Luftdruck Vorne Rechts Sollwert -channel-type.mybmw.rear-left-current-channel.label = Reifen Luftdruck Hinten Links -channel-type.mybmw.rear-left-wanted-channel.label = Reifen Luftdruck Hinten Links Sollwert -channel-type.mybmw.rear-right-current-channel.label = Reifen Luftdruck Hinten Rechts -channel-type.mybmw.rear-right-wanted-channel.label = Reifen Luftdruck Hinten Rechts Sollwert - +channel-type.mybmw.timer4-day-thu-channel.description = Donnerstag geplant für Zeitprofil 4 +channel-type.mybmw.timer4-day-thu-channel.label = Zeitprofil 4 - Donnerstag +channel-type.mybmw.timer4-day-tue-channel.description = Dienstag geplant für Zeitprofil 4 +channel-type.mybmw.timer4-day-tue-channel.label = Zeitprofil 4 - Dienstag +channel-type.mybmw.timer4-day-wed-channel.description = Mittwoch geplant für Zeitprofil 4 +channel-type.mybmw.timer4-day-wed-channel.label = Zeitprofil 4 - Mittwoch +channel-type.mybmw.timer4-departure-channel.description = Abfahrtszeit für Zeitprofil 4 +channel-type.mybmw.timer4-departure-channel.label = Zeitprofil 4 - Abfahrtszeit +channel-type.mybmw.timer4-departure-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.timer4-enabled-channel.description = Timer 4 aktiviert +channel-type.mybmw.timer4-enabled-channel.label = Zeitprofil 4 - Aktiviert +channel-type.mybmw.trunk-channel.label = Heckklappe +channel-type.mybmw.window-driver-front-channel.label = Fahrertür Fenster +channel-type.mybmw.window-driver-rear-channel.label = Fahrertür Hinten Fenster +channel-type.mybmw.window-end-channel.description = Ende der Ladezeit +channel-type.mybmw.window-end-channel.label = Ladefenster Endzeit +channel-type.mybmw.window-end-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.window-passenger-front-channel.label = Beifahrertür Fenster +channel-type.mybmw.window-passenger-rear-channel.label = Beifahrertür Hinten Fenster +channel-type.mybmw.window-start-channel.description = Start der Ladezeit +channel-type.mybmw.window-start-channel.label = Ladefenster Startzeit +channel-type.mybmw.window-start-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.windows-channel.label = Gesamtzustand der Fenster diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml index c3a79423fa0eb..72992e7846b54 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/conv-range-channel-group.xml @@ -10,6 +10,8 @@ + + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml index 39d9ca4e8fbc1..2c525f57d86a8 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml @@ -15,9 +15,9 @@ - - + + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml index b65089d6925ad..2490b56255abf 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml @@ -16,6 +16,8 @@ + + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml index b758b425ac876..6caf17952e62b 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/image-channel-types.xml @@ -14,9 +14,10 @@ - - - + + + + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml index 7bcfadde4ac99..c3246232a256e 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml @@ -33,6 +33,16 @@ + + Number:Dimensionless + + + + + Number:Dimensionless + + + Number:Length diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml index cb2a01615534b..3e651c02afa6c 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-groups.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - Current and wanted pressure for all tires + Current and target pressure for all tires diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml index 72087c0ad7300..cc0bf7f5d97a9 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml @@ -37,24 +37,24 @@ - - String - - + + Number:Time + + String - - Switch - - - DateTime - + + + + + DateTime + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml index 0c3982aa68961..318b0bad0602e 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-group.xml @@ -13,8 +13,8 @@ - + diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java deleted file mode 100644 index edbc86ae0805d..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/DiscoveryTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.discovery; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; -import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler; -import org.openhab.binding.mybmw.internal.util.FileReader; -import org.openhab.binding.mybmw.internal.utils.Constants; -import org.openhab.binding.mybmw.internal.utils.Converter; -import org.openhab.core.config.discovery.DiscoveryListener; -import org.openhab.core.config.discovery.DiscoveryResult; -import org.openhab.core.config.discovery.DiscoveryService; -import org.openhab.core.io.net.http.HttpClientFactory; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.ThingUID; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - -/** - * The {@link DiscoveryTest} Test Discovery Results - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class DiscoveryTest { - - @Test - public void testDiscovery() { - String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json"); - Bridge b = mock(Bridge.class); - MyBMWBridgeHandler bh = new MyBMWBridgeHandler(b, mock(HttpClientFactory.class), "en"); - when(b.getUID()).thenReturn(new ThingUID("mybmw", "account", "abc")); - VehicleDiscovery discovery = new VehicleDiscovery(); - discovery.setThingHandler(bh); - DiscoveryListener listener = mock(DiscoveryListener.class); - discovery.addDiscoveryListener(listener); - List vl = Converter.getVehicleList(content); - assertEquals(1, vl.size(), "Vehicles found"); - ArgumentCaptor discoveries = ArgumentCaptor.forClass(DiscoveryResult.class); - ArgumentCaptor services = ArgumentCaptor.forClass(DiscoveryService.class); - bh.onResponse(content); - verify(listener, times(1)).thingDiscovered(services.capture(), discoveries.capture()); - List results = discoveries.getAllValues(); - assertEquals(1, results.size(), "Found Vehicles"); - DiscoveryResult result = results.get(0); - assertEquals("mybmw:bev_rex:abc:anonymous", result.getThingUID().getAsString(), "Thing UID"); - } - - @Test - public void testProperties() { - String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json"); - Vehicle vehicle = Converter.getVehicle(Constants.ANONYMOUS, content); - String servicesSuppoertedReference = "RemoteHistory;ChargingHistory;ScanAndCharge;DCSContractManagement;BmwCharging;ChargeNowForBusiness;ChargingPlan"; - String servicesUnsuppoertedReference = "MiniCharging;EvGoCharging;CustomerEsim;CarSharing;EasyCharge"; - String servicesEnabledReference = "FindCharging;"; - String servicesDisabledReference = "DataPrivacy;ChargingSettings;ChargingHospitality;ChargingPowerLimit;ChargingTargetSoc;ChargingLoudness"; - assertEquals(servicesSuppoertedReference, - VehicleDiscovery.getServices(vehicle, VehicleDiscovery.SUPPORTED_SUFFIX, true), "Services supported"); - assertEquals(servicesUnsuppoertedReference, - VehicleDiscovery.getServices(vehicle, VehicleDiscovery.SUPPORTED_SUFFIX, false), - "Services unsupported"); - - String servicesEnabled = VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLED_SUFFIX, true) - + Constants.SEMICOLON + VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLE_SUFFIX, true); - assertEquals(servicesEnabledReference, servicesEnabled, "Services enabled"); - String servicesDisabled = VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLED_SUFFIX, false) - + Constants.SEMICOLON + VehicleDiscovery.getServices(vehicle, VehicleDiscovery.ENABLE_SUFFIX, false); - assertEquals(servicesDisabledReference, servicesDisabled, "Services disabled"); - } - - @Test - public void testAnonymousFingerPrint() { - String content = FileReader.readFileInString("src/test/resources/responses/fingerprint-raw.json"); - String anonymous = Converter.anonymousFingerprint(content); - assertFalse(anonymous.contains("ABC45678"), "VIN deleted"); - - anonymous = Converter.anonymousFingerprint(Constants.EMPTY); - assertEquals(Constants.EMPTY, anonymous, "Equal Fingerprint if Empty"); - - anonymous = Converter.anonymousFingerprint(Constants.EMPTY_JSON); - assertEquals(Constants.EMPTY_JSON, anonymous, "Equal Fingerprint if Empty JSon"); - } - - @Test - public void testRawVehicleData() { - String content = FileReader.readFileInString("src/test/resources/responses/TwoVehicles/two-vehicles.json"); - String anonymousVehicle = Converter.getRawVehicleContent("anonymous", content); - String contentAnon = FileReader.readFileInString("src/test/resources/responses/TwoVehicles/anonymous-raw.json"); - // remove formatting - JsonObject jo = JsonParser.parseString(contentAnon).getAsJsonObject(); - assertEquals(jo.toString(), anonymousVehicle, "Anonymous VIN raw data"); - String contentF11 = FileReader.readFileInString("src/test/resources/responses/TwoVehicles/f11-raw.json"); - String f11Vehicle = Converter.getRawVehicleContent("some_vin_F11", content); - jo = JsonParser.parseString(contentF11).getAsJsonObject(); - assertEquals(jo.toString(), f11Vehicle, "F11 VIN raw data"); - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscoveryTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscoveryTest.java new file mode 100644 index 0000000000000..533c32982ec5c --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscoveryTest.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.discovery; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.openhab.binding.mybmw.internal.MyBMWConstants; +import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler; +import org.openhab.binding.mybmw.internal.handler.backend.MyBMWHttpProxy; +import org.openhab.binding.mybmw.internal.handler.backend.NetworkException; +import org.openhab.binding.mybmw.internal.util.FileReader; +import org.openhab.core.config.core.Configuration; +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.Thing; +import org.openhab.core.thing.ThingUID; + +import com.google.gson.Gson; + +/** + * The {@link VehicleDiscoveryTest} Test Discovery Results + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - updates + */ +@NonNullByDefault +public class VehicleDiscoveryTest { + + @Test + public void testDiscovery() { + String content = FileReader.fileToString("responses/vehicles.json"); + List vehicleList = Arrays.asList(new Gson().fromJson(content, Vehicle[].class)); + + VehicleDiscovery vehicleDiscovery = new VehicleDiscovery(); + + MyBMWBridgeHandler bridgeHandler = mock(MyBMWBridgeHandler.class); + + List things = new ArrayList<>(); + + Thing thing1 = mock(Thing.class); + when(thing1.getConfiguration()).thenReturn(createConfiguration("VIN1234567")); + things.add(thing1); + Thing thing2 = mock(Thing.class); + when(thing2.getConfiguration()).thenReturn(createConfiguration("VIN1234568")); + things.add(thing2); + + Bridge bridge = mock(Bridge.class); + when(bridge.getUID()).thenReturn(new ThingUID("mybmw", "account", "abc")); + + when(bridgeHandler.getThing()).thenReturn(bridge); + + MyBMWHttpProxy myBMWProxy = mock(MyBMWHttpProxy.class); + try { + when(myBMWProxy.requestVehicles()).thenReturn(vehicleList); + } catch (NetworkException e) { + } + + when(bridgeHandler.getMyBmwProxy()).thenReturn(Optional.of(myBMWProxy)); + + vehicleDiscovery.setThingHandler(bridgeHandler); + assertNotNull(vehicleDiscovery.getThingHandler()); + + DiscoveryListener listener = mock(DiscoveryListener.class); + vehicleDiscovery.addDiscoveryListener(listener); + + assertEquals(2, vehicleList.size(), "Vehicles not found"); + ArgumentCaptor discoveries = ArgumentCaptor.forClass(DiscoveryResult.class); + ArgumentCaptor services = ArgumentCaptor.forClass(DiscoveryService.class); + + // call the discovery + vehicleDiscovery.startScan(); + + Mockito.verify(listener, Mockito.times(2)).thingDiscovered(services.capture(), discoveries.capture()); + List results = discoveries.getAllValues(); + assertEquals(2, results.size(), "Vehicles Not Found"); + + assertEquals("mybmw:conv:abc:VIN1234567", results.get(0).getThingUID().getAsString(), "Thing UID"); + assertEquals("mybmw:conv:abc:VIN1234568", results.get(1).getThingUID().getAsString(), "Thing UID"); + + // call the discovery again to check if the vehicle is already known -> no newly created vehicles should be + // found + when(bridge.getThings()).thenReturn(things); + + ArgumentCaptor discoveries2 = ArgumentCaptor.forClass(DiscoveryResult.class); + ArgumentCaptor services2 = ArgumentCaptor.forClass(DiscoveryService.class); + + // call the discovery + vehicleDiscovery.startScan(); + + Mockito.verify(listener, Mockito.times(2)).thingDiscovered(services2.capture(), discoveries2.capture()); + results = discoveries2.getAllValues(); + + vehicleDiscovery.deactivate(); + assertEquals(2, results.size(), "Vehicles Not Found"); + } + + private Configuration createConfiguration(String vin) { + Configuration configuration = new Configuration(); + configuration.put(MyBMWConstants.VIN, vin); + + return configuration; + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java deleted file mode 100644 index 817ff63def8a4..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeProfileTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Test; -import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; -import org.openhab.binding.mybmw.internal.util.FileReader; -import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper; -import org.openhab.binding.mybmw.internal.utils.Constants; -import org.openhab.binding.mybmw.internal.utils.Converter; - -/** - * The {@link ChargeProfileTest} is testing locale settings - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class ChargeProfileTest { - - @Test - public void testWeeklyPlanner() { - String json = FileReader - .readFileInString("src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json"); - Vehicle v = Converter.getVehicle(Constants.ANONYMOUS, json); - ChargeProfile cp = v.status.chargingProfile; - String cpJson = Converter.getGson().toJson(cp); - ChargeProfileWrapper cpw = new ChargeProfileWrapper(v.status.chargingProfile); - assertEquals(cpJson, cpw.getJson(), "JSON comparison"); - } - - @Test - public void testTwoWeeksPlanner() { - String json = FileReader.readFileInString("src/test/resources/responses/chargingprofile/two-weeks-timer.json"); - Vehicle v = Converter.getVehicle(Constants.ANONYMOUS, json); - ChargeProfile cp = v.status.chargingProfile; - String cpJson = Converter.getGson().toJson(cp); - ChargeProfileWrapper cpw = new ChargeProfileWrapper(v.status.chargingProfile); - assertEquals(cpJson, cpw.getJson(), "JSON comparison"); - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeStatisticWrapper.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargingStatisticsWrapper.java similarity index 67% rename from bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeStatisticWrapper.java rename to bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargingStatisticsWrapper.java index b582e1d3a96dc..977b6d90b126f 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargeStatisticWrapper.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/ChargingStatisticsWrapper.java @@ -12,8 +12,14 @@ */ package org.openhab.binding.mybmw.internal.dto; -import static org.junit.jupiter.api.Assertions.*; -import static org.openhab.binding.mybmw.internal.MyBMWConstants.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHARGE_STATISTICS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.ENERGY; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SESSIONS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.TITLE; import java.util.List; @@ -21,8 +27,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer; -import org.openhab.binding.mybmw.internal.utils.Converter; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; +import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; @@ -31,22 +37,18 @@ import org.openhab.core.types.State; /** - * The {@link ChargeStatisticWrapper} tests stored fingerprint responses from BMW API + * The {@link ChargingStatisticsWrapper} tests stored fingerprint responses from BMW API * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - small import change */ @NonNullByDefault @SuppressWarnings("null") -public class ChargeStatisticWrapper { - private ChargeStatisticsContainer chargeStatisticContainer; +public class ChargingStatisticsWrapper { + private ChargingStatisticsContainer chargeStatisticContainer; - public ChargeStatisticWrapper(String content) { - ChargeStatisticsContainer fromJson = Converter.getGson().fromJson(content, ChargeStatisticsContainer.class); - if (fromJson != null) { - chargeStatisticContainer = fromJson; - } else { - chargeStatisticContainer = new ChargeStatisticsContainer(); - } + public ChargingStatisticsWrapper(String content) { + chargeStatisticContainer = JsonStringDeserializer.getChargingStatistics(content); } /** @@ -79,7 +81,7 @@ private void checkResult(ChannelUID channelUID, State state) { st = (StringType) state; switch (gUid) { case CHANNEL_GROUP_CHARGE_STATISTICS: - assertEquals(chargeStatisticContainer.description, st.toString(), "Statistics name"); + assertEquals(chargeStatisticContainer.getDescription(), st.toString(), "Statistics name"); break; default: assertFalse(true, "Channel " + channelUID + " " + state + " not found"); @@ -89,14 +91,15 @@ private void checkResult(ChannelUID channelUID, State state) { case SESSIONS: assertTrue(state instanceof DecimalType); dt = ((DecimalType) state); - assertEquals(chargeStatisticContainer.statistics.numberOfChargingSessions, dt.intValue(), + assertEquals(chargeStatisticContainer.getStatistics().getNumberOfChargingSessions(), dt.intValue(), "Charge Sessions"); break; case ENERGY: assertTrue(state instanceof QuantityType); qte = ((QuantityType) state); assertEquals(Units.KILOWATT_HOUR, qte.getUnit(), "kwh"); - assertEquals(chargeStatisticContainer.statistics.totalEnergyCharged, qte.intValue(), "Energy"); + assertEquals(chargeStatisticContainer.getStatistics().getTotalEnergyCharged(), qte.intValue(), + "Energy"); break; default: // fail in case of unknown update diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java index c7f0f5f10c7fb..9b5d2f1ea40ca 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java @@ -12,8 +12,65 @@ */ package org.openhab.binding.mybmw.internal.dto; -import static org.junit.jupiter.api.Assertions.*; -import static org.openhab.binding.mybmw.internal.MyBMWConstants.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.ADDRESS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHARGE_PROFILE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHECK_CONTROL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_RANGE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_SERVICE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_STATUS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_REMAINING; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_STATUS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHECK_CONTROL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DATE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DETAILS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOORS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_DRIVER_FRONT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_DRIVER_REAR; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_PASSENGER_FRONT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_PASSENGER_REAR; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.ESTIMATED_FUEL_L_100KM; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.ESTIMATED_FUEL_MPG; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_LEFT_CURRENT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_LEFT_TARGET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_RIGHT_CURRENT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_RIGHT_TARGET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.GPS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.HEADING; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.HOME_DISTANCE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.HOOD; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.LAST_FETCHED; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.LAST_UPDATE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.LOCK; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.MILEAGE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.NAME; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.PLUG_CONNECTION; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_ELECTRIC; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_FUEL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_HYBRID; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_RADIUS_ELECTRIC; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_RADIUS_FUEL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_RADIUS_HYBRID; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.RAW; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_LEFT_CURRENT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_LEFT_TARGET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_RIGHT_CURRENT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_RIGHT_TARGET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMAINING_FUEL; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SERVICE_DATE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SERVICE_MILEAGE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SEVERITY; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SOC; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUNROOF; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.TRUNK; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOWS; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_DRIVER_FRONT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_DRIVER_REAR; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_PASSENGER_FRONT; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_PASSENGER_REAR; import java.util.HashMap; import java.util.List; @@ -25,18 +82,19 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType; -import org.openhab.binding.mybmw.internal.dto.properties.CBS; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; -import org.openhab.binding.mybmw.internal.handler.VehicleTests; +import org.openhab.binding.mybmw.internal.dto.vehicle.RequiredService; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleState; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.handler.VehicleHandlerTest; +import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer; import org.openhab.binding.mybmw.internal.utils.Constants; import org.openhab.binding.mybmw.internal.utils.Converter; import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils; import org.openhab.core.library.types.DateTimeType; -import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.DecimalType; 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.SIUnits; import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; @@ -47,13 +105,15 @@ * The {@link StatusWrapper} tests stored fingerprint responses from BMW API * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - updates for v2 API + * @author Mark Herwege - remaining charging time test */ @NonNullByDefault @SuppressWarnings("null") public class StatusWrapper { private static final Unit KILOMETRE = Constants.KILOMETRE_UNIT; - private Vehicle vehicle; + private VehicleState vehicleState; private boolean isElectric; private boolean hasFuel; private boolean isHybrid; @@ -66,13 +126,12 @@ public StatusWrapper(String type, String statusJson) { isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString()) || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString()); isHybrid = hasFuel && isElectric; - List vl = Converter.getVehicleList(statusJson); - assertEquals(1, vl.size(), "Vehciles found"); - vehicle = Converter.getConsistentVehcile(vl.get(0)); + VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(statusJson); + vehicleState = vehicleStateContainer.getState(); } /** - * Test results auctomatically against json values + * Test results automatically against json values * * @param channels * @param states @@ -108,8 +167,8 @@ private void checkResult(ChannelUID channelUID, State state) { StringType wanted; DateTimeType dtt; PointType pt; - OnOffType oot; - Unit wantedUnit; + DecimalType dt; + Unit wantedUnit = KILOMETRE; switch (cUid) { case MILEAGE: switch (gUid) { @@ -117,31 +176,20 @@ private void checkResult(ChannelUID channelUID, State state) { if (!state.equals(UnDefType.UNDEF)) { assertTrue(state instanceof QuantityType); qt = ((QuantityType) state); - if (Constants.KM_JSON.equals(vehicle.status.currentMileage.units)) { - assertEquals(KILOMETRE, qt.getUnit(), "KM"); - } else { - assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles"); - } - assertEquals(qt.intValue(), vehicle.status.currentMileage.mileage, "Mileage"); + assertEquals(qt.intValue(), vehicleState.getCurrentMileage(), "Mileage"); } else { - assertEquals(Constants.INT_UNDEF, vehicle.status.currentMileage.mileage, - "Mileage undefined"); + assertEquals(Constants.INT_UNDEF, vehicleState.getCurrentMileage(), "Mileage undefined"); } break; case CHANNEL_GROUP_SERVICE: - State wantedMileage = QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT); - if (!vehicle.properties.serviceRequired.isEmpty()) { - if (vehicle.properties.serviceRequired.get(0).distance != null) { - if (vehicle.properties.serviceRequired.get(0).distance.units - .equals(Constants.KILOMETERS_JSON)) { - wantedMileage = QuantityType.valueOf( - vehicle.properties.serviceRequired.get(0).distance.value, - Constants.KILOMETRE_UNIT); - } else { - wantedMileage = QuantityType.valueOf( - vehicle.properties.serviceRequired.get(0).distance.value, - ImperialUnits.MILE); - } + State wantedMileage = UnDefType.UNDEF; + if (!vehicleState.getRequiredServices().isEmpty()) { + if (vehicleState.getRequiredServices().get(0).getMileage() > 0) { + wantedMileage = QuantityType.valueOf( + vehicleState.getRequiredServices().get(0).getMileage(), + Constants.KILOMETRE_UNIT); + } else { + wantedMileage = UnDefType.UNDEF; } } assertEquals(wantedMileage, state, "Service Mileage"); @@ -155,51 +203,87 @@ private void checkResult(ChannelUID channelUID, State state) { assertTrue(isElectric, "Is Electric"); assertTrue(state instanceof QuantityType); qt = ((QuantityType) state); - wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators); assertEquals(wantedUnit, qt.getUnit()); - assertEquals(VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), qt.intValue(), - "Range Electric"); + assertEquals(vehicleState.getElectricChargingState().getRange(), qt.intValue(), "Range Electric"); break; - case RANGE_FUEL: - assertTrue(hasFuel, "Has Fuel"); + case RANGE_HYBRID: + assertTrue(isHybrid, "Is hybrid"); assertTrue(state instanceof QuantityType); qt = ((QuantityType) state); - wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators); assertEquals(wantedUnit, qt.getUnit()); - assertEquals(VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), qt.intValue(), - "Range Combustion"); + assertEquals(vehicleState.getRange(), qt.intValue(), "Range combined hybrid"); break; - case RANGE_HYBRID: - assertTrue(isHybrid, "Is Hybrid"); + case RANGE_FUEL: + assertTrue(hasFuel, "Has Fuel"); assertTrue(state instanceof QuantityType); qt = ((QuantityType) state); - wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators); - assertEquals(wantedUnit, qt.getUnit()); - assertEquals(VehicleStatusUtils.getRange(Constants.PHEV, vehicle), qt.intValue(), "Range Combined"); + if (!isHybrid) { + assertEquals(vehicleState.getCombustionFuelLevel().getRange(), qt.intValue(), "Range Combustion"); + } else { + assertEquals( + vehicleState.getCombustionFuelLevel().getRange() + - vehicleState.getElectricChargingState().getRange(), + qt.intValue(), "Range Combustion"); + } break; case REMAINING_FUEL: assertTrue(hasFuel, "Has Fuel"); assertTrue(state instanceof QuantityType); qt = ((QuantityType) state); assertEquals(Units.LITRE, qt.getUnit(), "Liter Unit"); - assertEquals(vehicle.properties.fuelLevel.value, qt.intValue(), "Fuel Level"); + assertEquals(vehicleState.getCombustionFuelLevel().getRemainingFuelLiters(), qt.intValue(), + "Fuel Level"); + break; + case ESTIMATED_FUEL_L_100KM: + assertTrue(hasFuel, "Has Fuel"); + + if (vehicleState.getCombustionFuelLevel().getRemainingFuelLiters() > 0 + && vehicleState.getCombustionFuelLevel().getRange() > 0) { + assertTrue(state instanceof DecimalType); + dt = ((DecimalType) state); + double estimatedFuelConsumptionL100km = vehicleState.getCombustionFuelLevel() + .getRemainingFuelLiters() * 1.0 / vehicleState.getCombustionFuelLevel().getRange() * 100.0; + assertEquals(estimatedFuelConsumptionL100km, dt.doubleValue(), + "Estimated Fuel Consumption l/100km"); + } else { + assertTrue(state instanceof UnDefType); + } + break; + case ESTIMATED_FUEL_MPG: + assertTrue(hasFuel, "Has Fuel"); + + if (vehicleState.getCombustionFuelLevel().getRemainingFuelLiters() > 0 + && vehicleState.getCombustionFuelLevel().getRange() > 0) { + assertTrue(state instanceof DecimalType); + dt = ((DecimalType) state); + double estimatedFuelConsumptionMpg = 235.214583 + / (vehicleState.getCombustionFuelLevel().getRemainingFuelLiters() * 1.0 + / vehicleState.getCombustionFuelLevel().getRange() * 100.0); + assertEquals(estimatedFuelConsumptionMpg, dt.doubleValue(), "Estimated Fuel Consumption mpg"); + } else { + assertTrue(state instanceof UnDefType); + } break; case SOC: - assertTrue(isElectric, "Is Ee wantedQt = (QuantityType) VehicleStatusUtils - .getNextServiceMileage(vehicle.properties.serviceRequired); + .getNextServiceMileage(vehicleState.getRequiredServices()); assertEquals(wantedQt.getUnit(), qt.getUnit(), "Next Service Miles"); assertEquals(wantedQt.intValue(), qt.intValue(), "Mileage"); } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) { - assertEquals(vehicle.properties.serviceRequired.get(0).distance.units, qt.getUnit(), - "First Service Unit"); - assertEquals(vehicle.properties.serviceRequired.get(0).distance.value, qt.intValue(), + assertEquals(vehicleState.getRequiredServices().get(0).getMileage(), qt.intValue(), "First Service Mileage"); } } @@ -410,16 +498,17 @@ private void checkResult(ChannelUID channelUID, State state) { switch (gUid) { case CHANNEL_GROUP_SERVICE: wanted = StringType.valueOf(Constants.NO_ENTRIES); - if (!vehicle.properties.serviceRequired.isEmpty()) { - wanted = StringType - .valueOf(Converter.toTitleCase(vehicle.properties.serviceRequired.get(0).type)); + if (!vehicleState.getRequiredServices().isEmpty()) { + wanted = StringType.valueOf( + Converter.toTitleCase(vehicleState.getRequiredServices().get(0).getType())); } assertEquals(wanted.toString(), st.toString(), "Service Name"); break; case CHANNEL_GROUP_CHECK_CONTROL: wanted = StringType.valueOf(Constants.NO_ENTRIES); - if (!vehicle.status.checkControlMessages.isEmpty()) { - wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).title); + if (!vehicleState.getCheckControlMessages().isEmpty()) { + wanted = StringType.valueOf( + Converter.toTitleCase(vehicleState.getCheckControlMessages().get(0).getType())); } assertEquals(wanted.toString(), st.toString(), "CheckControl Name"); break; @@ -434,16 +523,15 @@ private void checkResult(ChannelUID channelUID, State state) { switch (gUid) { case CHANNEL_GROUP_SERVICE: wanted = StringType.valueOf(Converter.toTitleCase(Constants.NO_ENTRIES)); - if (!vehicle.properties.serviceRequired.isEmpty()) { - wanted = StringType - .valueOf(Converter.toTitleCase(vehicle.properties.serviceRequired.get(0).type)); + if (!vehicleState.getRequiredServices().isEmpty()) { + wanted = StringType.valueOf(vehicleState.getRequiredServices().get(0).getDescription()); } assertEquals(wanted.toString(), st.toString(), "Service Details"); break; case CHANNEL_GROUP_CHECK_CONTROL: wanted = StringType.valueOf(Constants.NO_ENTRIES); - if (!vehicle.status.checkControlMessages.isEmpty()) { - wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).longDescription); + if (!vehicleState.getCheckControlMessages().isEmpty()) { + wanted = StringType.valueOf(vehicleState.getCheckControlMessages().get(0).getDescription()); } assertEquals(wanted.toString(), st.toString(), "CheckControl Details"); break; @@ -456,24 +544,24 @@ private void checkResult(ChannelUID channelUID, State state) { assertTrue(state instanceof StringType); st = (StringType) state; wanted = StringType.valueOf(Constants.NO_ENTRIES); - if (!vehicle.status.checkControlMessages.isEmpty()) { - wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).state); + if (!vehicleState.getCheckControlMessages().isEmpty()) { + wanted = StringType.valueOf( + Converter.toTitleCase(vehicleState.getCheckControlMessages().get(0).getSeverity())); } - assertEquals(wanted.toString(), st.toString(), "CheckControl Details"); + assertEquals(wanted.toString(), st.toString(), "CheckControl Severity"); break; case DATE: if (state.equals(UnDefType.UNDEF)) { - for (CBS serviceEntry : vehicle.properties.serviceRequired) { - assertTrue(serviceEntry.dateTime == null, "No Service Date available"); + for (RequiredService serviceEntry : vehicleState.getRequiredServices()) { + assertTrue(serviceEntry.getDateTime() == null, "No Service Date available"); } } else { assertTrue(state instanceof DateTimeType); dtt = (DateTimeType) state; switch (gUid) { case CHANNEL_GROUP_SERVICE: - String dueDateString = vehicle.properties.serviceRequired.get(0).dateTime; - DateTimeType expectedDTT = DateTimeType - .valueOf(Converter.zonedToLocalDateTime(dueDateString)); + String dueDateString = vehicleState.getRequiredServices().get(0).getDateTime(); + State expectedDTT = Converter.zonedToLocalDateTime(dueDateString); assertEquals(expectedDTT.toString(), dtt.toString(), "ServiceSate"); break; default: @@ -483,98 +571,89 @@ private void checkResult(ChannelUID channelUID, State state) { } break; case FRONT_LEFT_CURRENT: - if (vehicle.properties.tires != null) { + if (vehicleState.getTireState().getFrontLeft().getStatus().getCurrentPressure() > 0) { assertTrue(state instanceof QuantityType); qt = (QuantityType) state; - assertEquals(vehicle.properties.tires.frontLeft.status.currentPressure / 100, qt.doubleValue(), - "Fron Left Current"); + assertEquals(vehicleState.getTireState().getFrontLeft().getStatus().getCurrentPressure() / 100.0, + qt.doubleValue(), "Fron Left Current"); } else { - assertTrue(state.equals(UnDefType.UNDEF)); + assertEquals(state, UnDefType.UNDEF); } break; case FRONT_LEFT_TARGET: - if (vehicle.properties.tires != null) { + if (vehicleState.getTireState().getFrontLeft().getStatus().getTargetPressure() > 0) { assertTrue(state instanceof QuantityType); qt = (QuantityType) state; - assertEquals(vehicle.properties.tires.frontLeft.status.targetPressure / 100, qt.doubleValue(), - "Fron Left Current"); + assertEquals(vehicleState.getTireState().getFrontLeft().getStatus().getTargetPressure() / 100.0, + qt.doubleValue(), "Fron Left Current"); } else { assertTrue(state.equals(UnDefType.UNDEF)); } break; case FRONT_RIGHT_CURRENT: - if (vehicle.properties.tires != null) { + if (vehicleState.getTireState().getFrontRight().getStatus().getCurrentPressure() > 0) { assertTrue(state instanceof QuantityType); qt = (QuantityType) state; - assertEquals(vehicle.properties.tires.frontRight.status.currentPressure / 100, qt.doubleValue(), - "Fron Left Current"); + assertEquals(vehicleState.getTireState().getFrontRight().getStatus().getCurrentPressure() / 100.0, + qt.doubleValue(), "Fron Left Current"); } else { assertTrue(state.equals(UnDefType.UNDEF)); } break; case FRONT_RIGHT_TARGET: - if (vehicle.properties.tires != null) { + if (vehicleState.getTireState().getFrontRight().getStatus().getTargetPressure() > 0) { assertTrue(state instanceof QuantityType); qt = (QuantityType) state; - assertEquals(vehicle.properties.tires.frontRight.status.targetPressure / 100, qt.doubleValue(), - "Fron Left Current"); + assertEquals(vehicleState.getTireState().getFrontRight().getStatus().getTargetPressure() / 100.0, + qt.doubleValue(), "Fron Left Current"); } else { assertTrue(state.equals(UnDefType.UNDEF)); } break; case REAR_LEFT_CURRENT: - if (vehicle.properties.tires != null) { + if (vehicleState.getTireState().getRearLeft().getStatus().getCurrentPressure() > 0) { assertTrue(state instanceof QuantityType); qt = (QuantityType) state; - assertEquals(vehicle.properties.tires.rearLeft.status.currentPressure / 100, qt.doubleValue(), - "Fron Left Current"); + assertEquals(vehicleState.getTireState().getRearLeft().getStatus().getCurrentPressure() / 100.0, + qt.doubleValue(), "Fron Left Current"); } else { assertTrue(state.equals(UnDefType.UNDEF)); } break; case REAR_LEFT_TARGET: - if (vehicle.properties.tires != null) { + if (vehicleState.getTireState().getRearLeft().getStatus().getTargetPressure() > 0) { assertTrue(state instanceof QuantityType); qt = (QuantityType) state; - assertEquals(vehicle.properties.tires.rearLeft.status.targetPressure / 100, qt.doubleValue(), - "Fron Left Current"); + assertEquals(vehicleState.getTireState().getRearLeft().getStatus().getTargetPressure() / 100.0, + qt.doubleValue(), "Fron Left Current"); } else { assertTrue(state.equals(UnDefType.UNDEF)); } break; case REAR_RIGHT_CURRENT: - if (vehicle.properties.tires != null) { + if (vehicleState.getTireState().getRearRight().getStatus().getCurrentPressure() > 0) { assertTrue(state instanceof QuantityType); qt = (QuantityType) state; - assertEquals(vehicle.properties.tires.rearRight.status.currentPressure / 100, qt.doubleValue(), - "Fron Left Current"); + assertEquals(vehicleState.getTireState().getRearRight().getStatus().getCurrentPressure() / 100.0, + qt.doubleValue(), "Fron Left Current"); } else { assertTrue(state.equals(UnDefType.UNDEF)); } break; case REAR_RIGHT_TARGET: - if (vehicle.properties.tires != null) { + if (vehicleState.getTireState().getRearRight().getStatus().getTargetPressure() > 0) { assertTrue(state instanceof QuantityType); qt = (QuantityType) state; - assertEquals(vehicle.properties.tires.rearRight.status.targetPressure / 100, qt.doubleValue(), - "Fron Left Current"); + assertEquals(vehicleState.getTireState().getRearRight().getStatus().getTargetPressure() / 100.0, + qt.doubleValue(), "Fron Left Current"); } else { assertTrue(state.equals(UnDefType.UNDEF)); } break; - case MOTION: - assertTrue(state instanceof OnOffType); - oot = (OnOffType) state; - if (vehicle.properties.inMotion) { - assertEquals(oot.toFullString(), OnOffType.ON.toFullString(), "Vehicle Driving"); - } else { - assertEquals(oot.toFullString(), OnOffType.OFF.toFullString(), "Vehicle Stationary"); - } - break; case ADDRESS: if (state instanceof StringType) { st = (StringType) state; - assertEquals(st.toFullString(), vehicle.properties.vehicleLocation.address.formatted, + assertEquals(st.toFullString(), vehicleState.getLocation().getAddress().getFormatted(), "Location Address"); } // else no check needed break; @@ -582,9 +661,9 @@ private void checkResult(ChannelUID channelUID, State state) { if (state instanceof QuantityType) { qt = (QuantityType) state; PointType vehicleLocation = PointType - .valueOf(Double.toString(vehicle.properties.vehicleLocation.coordinates.latitude) + "," - + Double.toString(vehicle.properties.vehicleLocation.coordinates.longitude)); - int distance = vehicleLocation.distanceFrom(VehicleTests.HOME_LOCATION).intValue(); + .valueOf(Double.toString(vehicleState.getLocation().getCoordinates().getLatitude()) + "," + + Double.toString(vehicleState.getLocation().getCoordinates().getLongitude())); + int distance = vehicleLocation.distanceFrom(VehicleHandlerTest.HOME_LOCATION).intValue(); assertEquals(qt.intValue(), distance, "Distance from Home"); assertEquals(qt.getUnit(), SIUnits.METRE, "Distance from Home Unit"); } // else no check needed diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java index ece7d6778dcfb..c909f82361657 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehiclePropertiesTest.java @@ -18,10 +18,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; -import org.openhab.binding.mybmw.internal.util.FileReader; -import org.openhab.binding.mybmw.internal.utils.Constants; -import org.openhab.binding.mybmw.internal.utils.Converter; import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.types.CommandOption; @@ -30,24 +26,11 @@ * The {@link VehiclePropertiesTest} tests stored fingerprint responses from BMW API * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - move test to myBMWProxyTest */ @NonNullByDefault public class VehiclePropertiesTest { - @Test - public void testUserInfo() { - String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json"); - List vl = Converter.getVehicleList(content); - - assertEquals(1, vl.size(), "Number of Vehicles"); - Vehicle v = vl.get(0); - assertEquals(Constants.ANONYMOUS, v.vin, "VIN"); - assertEquals("i3 94 (+ REX)", v.model, "Model"); - assertEquals(Constants.BEV, v.driveTrain, "DriveTrain"); - assertEquals("BMW", v.brand, "Brand"); - assertEquals(2017, v.year, "Year of Construction"); - } - @Test public void testChannelUID() { ThingTypeUID thingTypePHEV = new ThingTypeUID("mybmw", "plugin-hybrid-vehicle"); @@ -56,7 +39,7 @@ public void testChannelUID() { @Test public void testRemoteServiceOptions() { - String commandReference = "[CommandOption [command=light-flash, label=Flash Lights], CommandOption [command=vehicle-finder, label=Vehicle Finder], CommandOption [command=door-lock, label=Door Lock], CommandOption [command=door-unlock, label=Door Unlock], CommandOption [command=horn-blow, label=Horn Blow], CommandOption [command=climate-now-start, label=Start Climate], CommandOption [command=climate-now-stop, label=Stop Climate]]"; + String commandReference = "[CommandOption [command=light-flash, label=Flash Lights], CommandOption [command=vehicle-finder, label=Vehicle Finder], CommandOption [command=door-lock, label=Door Lock], CommandOption [command=door-unlock, label=Door Unlock], CommandOption [command=horn-blow, label=Horn Blow], CommandOption [command=climate-now-start, label=Start Climate], CommandOption [command=climate-now-stop, label=Stop Climate], CommandOption [command=charge-now, label=Charge]]"; List l = RemoteServiceUtils.getOptions(true); assertEquals(commandReference, l.toString(), "Commad Options"); } diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java deleted file mode 100644 index 3de394d8305b5..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/VehicleStatusTest.java +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.dto; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Test; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; -import org.openhab.binding.mybmw.internal.util.FileReader; -import org.openhab.binding.mybmw.internal.utils.Constants; -import org.openhab.binding.mybmw.internal.utils.Converter; -import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils; -import org.openhab.core.library.types.DateTimeType; - -/** - * The {@link VehicleStatusTest} tests stored fingerprint responses from BMW API - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -@SuppressWarnings("null") -public class VehicleStatusTest { - - @Test - public void testServiceDate() { - String json = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json"); - Vehicle v = Converter.getVehicle(Constants.ANONYMOUS, json); - assertEquals(Constants.ANONYMOUS, v.vin, "VIN check"); - assertEquals("2023-11-01T00:00", - ((DateTimeType) VehicleStatusUtils.getNextServiceDate(v.properties.serviceRequired)).getZonedDateTime() - .toLocalDateTime().toString(), - "Service Date"); - - ZonedDateTime zdt = ZonedDateTime.parse("2021-12-21T16:46:02Z").withZoneSameInstant(ZoneId.systemDefault()); - LocalDateTime ldt = zdt.toLocalDateTime(); - assertEquals(ldt.format(Converter.DATE_INPUT_PATTERN), - Converter.zonedToLocalDateTime(v.properties.lastUpdatedAt), "Last update time"); - } - - @Test - public void testBevRexValues() { - String vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json"); - List vehicleList = Converter.getVehicleList(vehiclesJSON); - assertEquals(1, vehicleList.size(), "Vehicles found"); - Vehicle v = vehicleList.get(0); - assertEquals("BMW", v.brand, "Car brand"); - assertEquals(true, v.properties.areDoorsClosed, "Doors Closed"); - assertEquals(76, v.properties.electricRange.distance.value, "Electric Range"); - assertEquals(9.876, v.properties.vehicleLocation.coordinates.longitude, 0.1, "Location lon"); - assertEquals("immediateCharging", v.status.chargingProfile.chargingMode, "Charging Mode"); - assertEquals(2, v.status.chargingProfile.getTimerId(2).id, "Timer ID"); - assertEquals("[sunday]", v.status.chargingProfile.getTimerId(2).timerWeekDays.toString(), "Timer Weekdays"); - } - - @Test - public void testGuessRange() { - /** - * PHEV G01 - * fuelIndicator electric unit = % - * fuelIndicator fuel unit = l - * fuelIndicator hybrid unit = null - */ - String vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/G01/vehicles_v2_bmw_0.json"); - List vehicleList = Converter.getVehicleList(vehiclesJSON); - assertEquals(1, vehicleList.size(), "Vehicles found"); - Vehicle vehicle = vehicleList.get(0); - assertEquals(2, VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), "Electric Range"); - assertEquals(437, VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), "Fuel Range"); - assertEquals(439, VehicleStatusUtils.getRange(Constants.PHEV, vehicle), "Hybrid Range"); - - /** - * Electric REX I01 - * fuelIndicator electric unit = % - * fuelIndicator fuel unit = null - * fuelIndicator hybrid unit = null - */ - vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json"); - vehicleList = Converter.getVehicleList(vehiclesJSON); - assertEquals(1, vehicleList.size(), "Vehicles found"); - vehicle = vehicleList.get(0); - assertEquals(164, VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), "Electric Range"); - assertEquals(64, VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), "Fuel Range"); - assertEquals(228, VehicleStatusUtils.getRange(Constants.PHEV, vehicle), "Hybrid Range"); - - /** - * PHEV G05 - * fuelIndicator electric unit = % - * fuelIndicator fuel unit = % - * fuelIndicator hybrid unit = null - */ - vehiclesJSON = FileReader.readFileInString("src/test/resources/responses/G05/vehicles_v2_bmw_0.json"); - vehicleList = Converter.getVehicleList(vehiclesJSON); - assertEquals(1, vehicleList.size(), "Vehicles found"); - vehicle = vehicleList.get(0); - assertEquals(48, VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), "Electric Range"); - assertEquals(418, VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), "Fuel Range"); - assertEquals(466, VehicleStatusUtils.getRange(Constants.PHEV, vehicle), "Hybrid Range"); - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ChargeStatisticsTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsTest.java similarity index 52% rename from bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ChargeStatisticsTest.java rename to bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsTest.java index a1d4a8816110d..18c5e08ca33bb 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ChargeStatisticsTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsTest.java @@ -10,11 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mybmw.internal.handler; +package org.openhab.binding.mybmw.internal.dto.charge; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.Optional; @@ -24,8 +31,11 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType; -import org.openhab.binding.mybmw.internal.VehicleConfiguration; -import org.openhab.binding.mybmw.internal.dto.ChargeStatisticWrapper; +import org.openhab.binding.mybmw.internal.MyBMWVehicleConfiguration; +import org.openhab.binding.mybmw.internal.dto.ChargingStatisticsWrapper; +import org.openhab.binding.mybmw.internal.handler.MyBMWCommandOptionProvider; +import org.openhab.binding.mybmw.internal.handler.VehicleHandler; +import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer; import org.openhab.binding.mybmw.internal.util.FileReader; import org.openhab.binding.mybmw.internal.utils.Constants; import org.openhab.core.i18n.LocationProvider; @@ -38,14 +48,16 @@ import org.slf4j.LoggerFactory; /** - * The {@link ChargeStatisticsTest} is responsible for handling commands, which are + * The {@link ChargingStatisticsTest} is responsible for handling commands, which + * are * sent to one of the channels. * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - updated to new vehicles */ @NonNullByDefault @SuppressWarnings("null") -public class ChargeStatisticsTest { +public class ChargingStatisticsTest { private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class); private static final int EXPECTED_UPDATE_COUNT = 3; @@ -55,9 +67,9 @@ public class ChargeStatisticsTest { @Nullable ArgumentCaptor stateCaptor; @Nullable - ThingHandlerCallback tc; + ThingHandlerCallback thingHandlerCallback; @Nullable - VehicleHandler cch; + VehicleHandler vehicleHandler; @Nullable List allChannels; @Nullable @@ -73,15 +85,22 @@ public void setup(String type, boolean imperial) { this.imperial = imperial; Thing thing = mock(Thing.class); when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test")); - MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class); + MyBMWCommandOptionProvider myBmwCommandOptionProvider = mock(MyBMWCommandOptionProvider.class); LocationProvider locationProvider = mock(LocationProvider.class); - cch = new VehicleHandler(thing, cop, locationProvider, type); - VehicleConfiguration vc = new VehicleConfiguration(); - vc.vin = Constants.ANONYMOUS; - Optional ovc = Optional.of(vc); - cch.configuration = ovc; - tc = mock(ThingHandlerCallback.class); - cch.setCallback(tc); + vehicleHandler = new VehicleHandler(thing, myBmwCommandOptionProvider, locationProvider, type); + MyBMWVehicleConfiguration vc = new MyBMWVehicleConfiguration(); + vc.setVin(Constants.ANONYMOUS); + + try { + Field vehicleConfigurationField = VehicleHandler.class.getDeclaredField("vehicleConfiguration"); + vehicleConfigurationField.setAccessible(true); + vehicleConfigurationField.set(vehicleHandler, Optional.of(vc)); + } catch (Exception e) { + logger.error("vehicleConfiguration could not be set", e); + fail("vehicleConfiguration could not be set", e); + } + thingHandlerCallback = mock(ThingHandlerCallback.class); + vehicleHandler.setCallback(thingHandlerCallback); channelCaptor = ArgumentCaptor.forClass(ChannelUID.class); stateCaptor = ArgumentCaptor.forClass(State.class); } @@ -89,13 +108,24 @@ public void setup(String type, boolean imperial) { private boolean testVehicle(String statusContent, int callbacksExpected, Optional> concreteChecks) { assertNotNull(statusContent); - cch.chargeStatisticsCallback.onResponse(statusContent); - verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture()); + + try { + Method updateChargeStatisticsMethod = VehicleHandler.class.getDeclaredMethod("updateChargingStatistics", + ChargingStatisticsContainer.class, String.class); + updateChargeStatisticsMethod.setAccessible(true); + updateChargeStatisticsMethod.invoke(vehicleHandler, + JsonStringDeserializer.getChargingStatistics(statusContent), null); + } catch (Exception e) { + logger.error("chargeStatistics could not be set", e); + fail("chargeStatistics could not be set", e); + } + verify(thingHandlerCallback, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), + stateCaptor.capture()); allChannels = channelCaptor.getAllValues(); allStates = stateCaptor.getAllValues(); assertNotNull(driveTrain); - ChargeStatisticWrapper checker = new ChargeStatisticWrapper(statusContent); + ChargingStatisticsWrapper checker = new ChargingStatisticsWrapper(statusContent); trace(); return checker.checkResults(allChannels, allStates); } @@ -107,35 +137,34 @@ private void trace() { } @Test - public void testI01REX() { + public void testBevIx() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); setup(VehicleType.ELECTRIC_REX.toString(), false); - String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/charge-statistics-de.json"); + String content = FileReader.fileToString("responses/BEV/charging_statistics.json"); assertTrue(testVehicle(content, EXPECTED_UPDATE_COUNT, Optional.empty())); } @Test - public void testG21() { + public void testBevIX3() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.PLUGIN_HYBRID.toString(), false); - String content = FileReader.readFileInString("src/test/resources/responses/G21/charging-statistics_0.json"); + setup(VehicleType.ELECTRIC_REX.toString(), false); + String content = FileReader.fileToString("responses/BEV3/charging_statistics.json"); assertTrue(testVehicle(content, EXPECTED_UPDATE_COUNT, Optional.empty())); } @Test - public void testG30() { + public void testIceX320d() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); setup(VehicleType.PLUGIN_HYBRID.toString(), false); - String content = FileReader.readFileInString("src/test/resources/responses/G30/charging-statistics_0.json"); + String content = FileReader.fileToString("responses/ICE2/charging_statistics.json"); assertTrue(testVehicle(content, EXPECTED_UPDATE_COUNT, Optional.empty())); } @Test - public void testI01NOREX() { + public void testPhev330e() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.ELECTRIC.toString(), false); - String content = FileReader - .readFileInString("src/test/resources/responses/I01_NOREX/charging-statistics_0.json"); + setup(VehicleType.PLUGIN_HYBRID.toString(), false); + String content = FileReader.fileToString("responses/PHEV2/charging_statistics.json"); assertTrue(testVehicle(content, EXPECTED_UPDATE_COUNT, Optional.empty())); } } diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java new file mode 100644 index 0000000000000..3e66b9981c998 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer; +import org.openhab.binding.mybmw.internal.util.FileReader; +import org.openhab.binding.mybmw.internal.utils.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +import ch.qos.logback.classic.Level; + +/** + * + * checks the vehicleBase response + * + * @author Martin Grassl - initial contribution + */ +public class VehicleBaseTest { + + private Logger logger = LoggerFactory.getLogger(VehicleBaseTest.class); + + @BeforeEach + public void setupLogger() { + Logger root = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + + if ("debug".equals(System.getenv("LOG_LEVEL"))) { + ((ch.qos.logback.classic.Logger) root).setLevel(Level.DEBUG); + } else if ("trace".equals(System.getenv("LOG_LEVEL"))) { + ((ch.qos.logback.classic.Logger) root).setLevel(Level.TRACE); + } + + logger.trace("tracing enabled"); + logger.debug("debugging enabled"); + logger.info("info enabled"); + } + + @Test + public void testVehicleBaseDeserializationByGson() { + String vehicleBaseJson = FileReader.fileToString("responses/MILD_HYBRID/vehicles_base.json"); + Gson gson = new Gson(); + + VehicleBase[] vehicleBaseArray = gson.fromJson(vehicleBaseJson, VehicleBase[].class); + + assertNotNull(vehicleBaseArray); + } + + @Test + public void testVehicleBaseDeserializationByConverter() { + String vehicleBaseJson = FileReader.fileToString("responses/MILD_HYBRID/vehicles_base.json"); + + List vehicleBaseList = JsonStringDeserializer.getVehicleBaseList(vehicleBaseJson); + + assertNotNull(vehicleBaseList); + + assertEquals(1, vehicleBaseList.size(), "Number of Vehicles"); + VehicleBase vehicle = vehicleBaseList.get(0); + assertEquals(Constants.ANONYMOUS + "MILD_HYBRID", vehicle.getVin(), "VIN"); + assertEquals("M340i xDrive", vehicle.getAttributes().getModel(), "Model"); + assertEquals(Constants.DRIVETRAIN_MILD_HYBRID, vehicle.getAttributes().getDriveTrain(), "DriveTrain"); + assertEquals("bmw", vehicle.getAttributes().getBrand(), "Brand"); + assertEquals(2022, vehicle.getAttributes().getYear(), "Year of Construction"); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilitiesTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilitiesTest.java new file mode 100644 index 0000000000000..e26baf1b271cb --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilitiesTest.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer; +import org.openhab.binding.mybmw.internal.util.FileReader; + +/** + * + * checks the transformation of the capabilities to string lists + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactoring + */ +public class VehicleCapabilitiesTest { + @Test + void testGetCapabilitiesAsString() { + String content = FileReader.fileToString("responses/BEV/vehicles_state.json"); + VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(content); + + String servicesSupportedReference = "BmwCharging;ChargingHistory;ChargingPlan;CustomerEsim;DCSContractManagement;RemoteHistory;ScanAndCharge"; + String servicesUnsupportedReference = "CarSharing;ChargeNowForBusiness;ClimateTimer;EvGoCharging;MiniCharging;RemoteEngineStart;RemoteHistoryDeletion;RemoteParking;Sustainability;WifiHotspotService"; + String servicesEnabledReference = "ChargingHospitality;ChargingLoudness;ChargingPowerLimit;ChargingSettings;ChargingTargetSoc"; + String servicesDisabledReference = "DataPrivacy;EasyCharge;NonLscFeature;SustainabilityAccumulatedView"; + assertEquals(servicesSupportedReference, vehicleStateContainer.getCapabilities() + .getCapabilitiesAsString(VehicleCapabilities.SUPPORTED_SUFFIX, true), "Services supported"); + assertEquals(servicesUnsupportedReference, vehicleStateContainer.getCapabilities() + .getCapabilitiesAsString(VehicleCapabilities.SUPPORTED_SUFFIX, false), "Services unsupported"); + assertEquals(servicesEnabledReference, vehicleStateContainer.getCapabilities() + .getCapabilitiesAsString(VehicleCapabilities.ENABLED_SUFFIX, true), "Services enabled"); + assertEquals(servicesDisabledReference, vehicleStateContainer.getCapabilities() + .getCapabilitiesAsString(VehicleCapabilities.ENABLED_SUFFIX, false), "Services disabled"); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java new file mode 100644 index 0000000000000..42414081d0360 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.dto.vehicle; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer; +import org.openhab.binding.mybmw.internal.util.FileReader; +import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils; +import org.openhab.core.library.types.DateTimeType; + +import com.google.gson.Gson; + +/** + * + * checks basic data of state of vehicle + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - refactoring + */ +public class VehicleStateContainerTest { + @Test + public void testVehicleStateDeserializationByGson() { + String vehicleStateJson = FileReader.fileToString("responses/MILD_HYBRID/vehicles_state.json"); + Gson gson = new Gson(); + + VehicleStateContainer vehicle = gson.fromJson(vehicleStateJson, VehicleStateContainer.class); + + assertNotNull(vehicle); + } + + @Test + public void testVehicleStateDeserializationByConverter() { + String vehicleStateJson = FileReader.fileToString("responses/MILD_HYBRID/vehicles_state.json"); + + VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(vehicleStateJson); + + assertNotNull(vehicleStateContainer); + assertEquals("2024-06-01T00:00", + ((DateTimeType) VehicleStatusUtils + .getNextServiceDate(vehicleStateContainer.getState().getRequiredServices())).getZonedDateTime() + .toLocalDateTime().toString(), + "Service Date"); + + assertEquals("2022-12-21T15:41:23Z", vehicleStateContainer.getState().getLastUpdatedAt(), "Last update time"); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ConfigurationTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ConfigurationTest.java deleted file mode 100644 index 5119966d01e0d..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ConfigurationTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler; - -import static org.junit.jupiter.api.Assertions.*; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Test; -import org.openhab.binding.mybmw.internal.MyBMWConfiguration; -import org.openhab.binding.mybmw.internal.utils.BimmerConstants; - -/** - * The {@link ConfigurationTest} test different configurations - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class ConfigurationTest { - - @Test - public void testAuthServerMap() { - MyBMWConfiguration cdc = new MyBMWConfiguration(); - assertFalse(MyBMWBridgeHandler.checkConfiguration(cdc)); - cdc.userName = "a"; - assertFalse(MyBMWBridgeHandler.checkConfiguration(cdc)); - cdc.password = "b"; - assertFalse(MyBMWBridgeHandler.checkConfiguration(cdc)); - cdc.region = "c"; - assertFalse(MyBMWBridgeHandler.checkConfiguration(cdc)); - cdc.region = BimmerConstants.REGION_NORTH_AMERICA; - assertTrue(MyBMWBridgeHandler.checkConfiguration(cdc)); - cdc.region = BimmerConstants.REGION_ROW; - assertTrue(MyBMWBridgeHandler.checkConfiguration(cdc)); - cdc.region = BimmerConstants.REGION_CHINA; - assertTrue(MyBMWBridgeHandler.checkConfiguration(cdc)); - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ErrorResponseTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ErrorResponseTest.java deleted file mode 100644 index bd30e4a125c95..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/ErrorResponseTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler; - -import static org.mockito.Mockito.*; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.openhab.core.i18n.LocationProvider; -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 channelCaptor; - @Nullable - ArgumentCaptor stateCaptor; - @Nullable - ThingHandlerCallback tc; - @Nullable - VehicleHandler cch; - @Nullable - List allChannels; - @Nullable - List allStates; - @Nullable - String driveTrain; - boolean imperial; - - /** - * Prepare environment for Vehicle Status Updates - */ - public void setup(String type, boolean imperial) { - driveTrain = type; - this.imperial = imperial; - Thing thing = mock(Thing.class); - when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test")); - MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class); - LocationProvider locationProvider = mock(LocationProvider.class); - cch = new VehicleHandler(thing, cop, locationProvider, type); - tc = mock(ThingHandlerCallback.class); - cch.setCallback(tc); - channelCaptor = ArgumentCaptor.forClass(ChannelUID.class); - stateCaptor = ArgumentCaptor.forClass(State.class); - } - - @Test - public void testErrorResponseCallbacks() { - String error = "{\"error\":true,\"reason\":\"offline\"}"; - setup("BEV", false); - cch.vehicleStatusCallback.onResponse(error); - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/SimulationTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/SimulationTest.java deleted file mode 100644 index d6522be29268e..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/SimulationTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler; - -import static org.junit.jupiter.api.Assertions.assertFalse; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Test; -import org.openhab.binding.mybmw.internal.handler.simulation.Injector; - -/** - * The {@link SimulationTest} Assures simulation is off - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class SimulationTest { - - @Test - public void testSimulationOff() { - assertFalse(Injector.isActive(), "Simulation off"); - } -} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleTests.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleHandlerTest.java similarity index 62% rename from bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleTests.java rename to bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleHandlerTest.java index ff800ed45c818..99752862e9a77 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleTests.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleHandlerTest.java @@ -12,9 +12,19 @@ */ package org.openhab.binding.mybmw.internal.handler; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -24,85 +34,113 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType; -import org.openhab.binding.mybmw.internal.VehicleConfiguration; +import org.openhab.binding.mybmw.internal.MyBMWVehicleConfiguration; import org.openhab.binding.mybmw.internal.dto.StatusWrapper; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer; import org.openhab.binding.mybmw.internal.util.FileReader; import org.openhab.binding.mybmw.internal.utils.Constants; import org.openhab.core.i18n.LocationProvider; import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.QuantityType; 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.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The {@link VehicleTests} is responsible for handling commands, which are + * The {@link VehicleHandlerTest} is responsible for handling commands, which are * sent to one of the channels. * * @author Bernd Weymann - Initial contribution */ @NonNullByDefault -@SuppressWarnings("null") -public class VehicleTests { +public class VehicleHandlerTest { private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class); + // counters for the number of properties per section private static final int STATUS_ELECTRIC = 12; private static final int STATUS_CONV = 9; - private static final int RANGE_HYBRID = 9; - private static final int RANGE_CONV = 4; + private static final int RANGE_HYBRID = 11; + private static final int RANGE_CONV = 6; private static final int RANGE_ELECTRIC = 4; private static final int DOORS = 11; private static final int CHECK_EMPTY = 3; - private static final int CHECK_AVAILABLE = 3; - private static final int SERVICE_AVAILABLE = 3; - private static final int SERVICE_EMPTY = 3; + private static final int SERVICE_AVAILABLE = 4; + private static final int SERVICE_EMPTY = 4; private static final int LOCATION = 4; private static final int CHARGE_PROFILE = 44; private static final int TIRES = 8; public static final PointType HOME_LOCATION = new PointType("54.321,9.876"); - @Nullable - ArgumentCaptor channelCaptor; - @Nullable - ArgumentCaptor stateCaptor; - @Nullable - ThingHandlerCallback tc; - @Nullable - VehicleHandler cch; - @Nullable - List allChannels; - @Nullable - List allStates; + + // I couldn't resolve all NonNull compile errors, hence I'm initializing the values here... + ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(ChannelUID.class); + ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(State.class); + ThingHandlerCallback thingHandlerCallback = mock(ThingHandlerCallback.class); + VehicleHandler vehicleHandler = mock(VehicleHandler.class); + List allChannels = new ArrayList<>(); + List allStates = new ArrayList<>(); + String driveTrain = Constants.EMPTY; /** * Prepare environment for Vehicle Status Updates */ - public void setup(String type, String vin) { + private void setup(String type, String vin) { driveTrain = type; Thing thing = mock(Thing.class); when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test")); MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class); LocationProvider locationProvider = mock(LocationProvider.class); when(locationProvider.getLocation()).thenReturn(HOME_LOCATION); - cch = new VehicleHandler(thing, cop, locationProvider, type); - VehicleConfiguration vc = new VehicleConfiguration(); - vc.vin = vin; - Optional ovc = Optional.of(vc); - cch.configuration = ovc; - tc = mock(ThingHandlerCallback.class); - cch.setCallback(tc); + vehicleHandler = new VehicleHandler(thing, cop, locationProvider, type); + MyBMWVehicleConfiguration vehicleConfiguration = new MyBMWVehicleConfiguration(); + vehicleConfiguration.setVin(vin); + + setVehicleConfigurationToVehicleHandler(vehicleHandler, vehicleConfiguration); + thingHandlerCallback = mock(ThingHandlerCallback.class); + try { + vehicleHandler.setCallback(thingHandlerCallback); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } channelCaptor = ArgumentCaptor.forClass(ChannelUID.class); stateCaptor = ArgumentCaptor.forClass(State.class); } + private void setVehicleConfigurationToVehicleHandler(@Nullable VehicleHandler vehicleHandler, + MyBMWVehicleConfiguration vehicleConfiguration) { + try { + Field vehicleConfigurationField = VehicleHandler.class.getDeclaredField("vehicleConfiguration"); + vehicleConfigurationField.setAccessible(true); + vehicleConfigurationField.set(vehicleHandler, Optional.of(vehicleConfiguration)); + } catch (Exception e) { + logger.error("vehicleConfiguration could not be set", e); + fail("vehicleConfiguration could not be set", e); + } + } + private boolean testVehicle(String statusContent, int callbacksExpected, Optional> concreteChecks) { assertNotNull(statusContent); - cch.vehicleStatusCallback.onResponse(statusContent); - verify(tc, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), stateCaptor.capture()); + + try { + Method triggerVehicleStatusUpdateMethod = VehicleHandler.class + .getDeclaredMethod("triggerVehicleStatusUpdate", VehicleStateContainer.class, String.class); + triggerVehicleStatusUpdateMethod.setAccessible(true); + triggerVehicleStatusUpdateMethod.invoke(vehicleHandler, + JsonStringDeserializer.getVehicleState(statusContent), null); + } catch (Exception e) { + logger.error("vehicleState could not be set", e); + fail("vehicleState could not be set", e); + } + + verify(thingHandlerCallback, times(callbacksExpected)).stateUpdated(channelCaptor.capture(), + stateCaptor.capture()); allChannels = channelCaptor.getAllValues(); allStates = stateCaptor.getAllValues(); @@ -123,6 +161,25 @@ private void trace() { } } + @Test + public void testPressureConversion() { + try { + Method calculatePressureMethod = VehicleHandler.class.getDeclaredMethod("calculatePressure", int.class); + calculatePressureMethod.setAccessible(true); + State state = (State) calculatePressureMethod.invoke(vehicleHandler, 110); + assertInstanceOf(QuantityType.class, state); + assertEquals(1.1, ((QuantityType) state).doubleValue()); + state = (State) calculatePressureMethod.invoke(vehicleHandler, 280); + assertEquals(2.8, ((QuantityType) state).doubleValue()); + + state = (State) calculatePressureMethod.invoke(vehicleHandler, -1); + assertInstanceOf(UnDefType.class, state); + } catch (Exception e) { + logger.error("vehicleState could not be set", e); + fail("vehicleState could not be set", e); + } + } + /** * Test various Vehicles from users which delivered their fingerprint. * The tests are checking the chain from "JSON to Channel update". @@ -211,132 +268,112 @@ private void trace() { * Channel testbinding::test:profile#timer4-day-sat OFF * Channel testbinding::test:profile#timer4-day-sun ON */ + @Test - public void testI01Rex() { + public void testBevIx() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.ELECTRIC_REX.toString(), Constants.ANONYMOUS); - String content = FileReader.readFileInString("src/test/resources/responses/I01_REX/vehicles.json"); - assertTrue(testVehicle(content, STATUS_ELECTRIC + RANGE_HYBRID + DOORS + LOCATION + SERVICE_AVAILABLE - + CHECK_EMPTY + CHARGE_PROFILE + TIRES, Optional.empty())); + setup(VehicleType.ELECTRIC.toString(), "anonymous"); + String content = FileReader.fileToString("responses/BEV/vehicles_state.json"); + assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY + + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); } @Test - public void testF11() { + public void testBevI3() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.CONVENTIONAL.toString(), "some_vin_F11"); - String content = FileReader.readFileInString("src/test/resources/responses/F11/vehicles_v2_bmw_0.json"); - assertTrue(testVehicle(content, - STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + LOCATION + TIRES, - Optional.empty())); + setup(VehicleType.ELECTRIC.toString(), "anonymous"); + String content = FileReader.fileToString("responses/BEV2/vehicles_state.json"); + assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY + + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); } @Test - public void testF31() { + public void testBevIX3() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.CONVENTIONAL.toString(), "some_vin_F31"); - String content = FileReader.readFileInString("src/test/resources/responses/F31/vehicles_v2_bmw_0.json"); - assertTrue(testVehicle(content, - STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_EMPTY + LOCATION + TIRES, - Optional.empty())); + setup(VehicleType.ELECTRIC.toString(), "anonymous"); + String content = FileReader.fileToString("responses/BEV3/vehicles_state.json"); + assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY + + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); } @Test - public void testF44() { + public void testBevI4() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.CONVENTIONAL.toString(), "some_vin_F44"); - String content = FileReader.readFileInString("src/test/resources/responses/F44/vehicles_v2_bmw_0.json"); - assertTrue(testVehicle(content, - STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty())); + setup(VehicleType.ELECTRIC.toString(), "anonymous"); + String content = FileReader.fileToString("responses/BEV4/vehicles_state.json"); + assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY + + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); } @Test - public void testF45() { + public void testBevI7() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_F45"); - String content = FileReader.readFileInString("src/test/resources/responses/F45/vehicles_v2_bmw_0.json"); - assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY + setup(VehicleType.ELECTRIC.toString(), "anonymous"); + String content = FileReader.fileToString("responses/BEV5/vehicles_state.json"); + assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); } @Test - public void testF48() { + public void testIceMiniCooper() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.CONVENTIONAL.toString(), "some_vin_F48"); - String content = FileReader.readFileInString("src/test/resources/responses/F48/vehicles_v2_bmw_0.json"); + setup(VehicleType.CONVENTIONAL.toString(), "anonymous"); + String content = FileReader.fileToString("responses/ICE/vehicles_state.json"); assertTrue(testVehicle(content, - STATUS_CONV + DOORS + RANGE_CONV + SERVICE_AVAILABLE + CHECK_AVAILABLE + LOCATION + TIRES, - Optional.empty())); + STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty())); } @Test - public void testG01() { + public void testIceX320d() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_G01"); - String content = FileReader.readFileInString("src/test/resources/responses/G01/vehicles_v2_bmw_0.json"); - assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY - + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); + setup(VehicleType.CONVENTIONAL.toString(), "anonymous"); + String content = FileReader.fileToString("responses/ICE2/vehicles_state.json"); + assertTrue(testVehicle(content, + STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty())); } @Test - public void testG05() { + public void testIce530d() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_G05"); - String content = FileReader.readFileInString("src/test/resources/responses/G05/vehicles_v2_bmw_0.json"); - assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY - + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); + setup(VehicleType.CONVENTIONAL.toString(), "anonymous"); + String content = FileReader.fileToString("responses/ICE3/vehicles_state.json"); + assertTrue(testVehicle(content, + STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty())); } @Test - public void testG08() { + public void testIce435i() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.ELECTRIC.toString(), "some_vin_G08"); - String content = FileReader.readFileInString("src/test/resources/responses/G08/vehicles_v2_bmw_0.json"); - assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY - + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); + setup(VehicleType.CONVENTIONAL.toString(), "anonymous"); + String content = FileReader.fileToString("responses/ICE4/vehicles_state.json"); + assertTrue(testVehicle(content, + STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty())); } @Test - public void testG21() { + public void testMildHybrid340i() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_G21"); - String content = FileReader.readFileInString("src/test/resources/responses/G21/vehicles_v2_bmw_0.json"); - assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY - + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); + setup(VehicleType.MILD_HYBRID.toString(), "anonymous"); + String content = FileReader.fileToString("responses/MILD_HYBRID/vehicles_state.json"); + assertTrue(testVehicle(content, + STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty())); } @Test - public void testG30() { + public void testPhev530e() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.PLUGIN_HYBRID.toString(), "some_vin_G30"); - String content = FileReader.readFileInString("src/test/resources/responses/G30/vehicles_v2_bmw_0.json"); + setup(VehicleType.PLUGIN_HYBRID.toString(), "anonymous"); + String content = FileReader.fileToString("responses/PHEV/vehicles_state.json"); assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); } @Test - public void testI01NoRex() { - logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.ELECTRIC.toString(), "some_vin_I01_NOREX"); - String content = FileReader.readFileInString("src/test/resources/responses/I01_NOREX/vehicles_v2_bmw_0.json"); - assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY - + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); - } - - @Test - public void test530e() { + public void testPhev330e() { logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); setup(VehicleType.PLUGIN_HYBRID.toString(), "anonymous"); - String content = FileReader.readFileInString("src/test/resources/responses/530e/vehicles.json"); + String content = FileReader.fileToString("responses/PHEV2/vehicles_state.json"); assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty())); } - - @Test - public void test340i() { - logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName()); - setup(VehicleType.MILD_HYBRID.toString(), "anonymous"); - String content = FileReader.readFileInString("src/test/resources/responses/G21/340i.json"); - assertTrue(testVehicle(content, - STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty())); - } } diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/AuthTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java similarity index 89% rename from bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/AuthTest.java rename to bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java index 081982168e7bf..14665529f640a 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/AuthTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java @@ -10,11 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mybmw.internal.handler; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; -import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.*; +package org.openhab.binding.mybmw.internal.handler.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.AUTHORIZATION; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CODE; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_JSON; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_URL_ENCODED; +import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_X_USER_AGENT; import java.nio.charset.StandardCharsets; import java.security.KeyFactory; @@ -35,12 +42,13 @@ import org.eclipse.jetty.util.UrlEncoded; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.Test; -import org.openhab.binding.mybmw.internal.MyBMWConfiguration; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse; import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse; import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse; import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration; import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse; +import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer; import org.openhab.binding.mybmw.internal.util.FileReader; import org.openhab.binding.mybmw.internal.utils.BimmerConstants; import org.openhab.binding.mybmw.internal.utils.Constants; @@ -53,12 +61,14 @@ * The {@link AuthTest} test authorization flow * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - moved to other package and updated for v2 */ @NonNullByDefault class AuthTest { private final Logger logger = LoggerFactory.getLogger(AuthTest.class); - void testAuth() { + @Test + public void testAuth() { String user = "usr"; String pwd = "pwd"; @@ -75,7 +85,7 @@ void testAuth() { ContentResponse firstResponse = firstRequest.send(); logger.info(firstResponse.getContentAsString()); - AuthQueryResponse aqr = Converter.getGson().fromJson(firstResponse.getContentAsString(), + AuthQueryResponse aqr = JsonStringDeserializer.deserializeString(firstResponse.getContentAsString(), AuthQueryResponse.class); // String verifier_bytes = RandomStringUtils.randomAlphanumeric(64); @@ -150,7 +160,8 @@ void testAuth() { UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); ContentResponse codeResponse = codeRequest.send(); logger.info(codeResponse.getContentAsString()); - AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class); + AuthResponse ar = JsonStringDeserializer.deserializeString(codeResponse.getContentAsString(), + AuthResponse.class); Token t = new Token(); t.setType(ar.tokenType); t.setToken(ar.accessToken); @@ -270,11 +281,11 @@ void testAuth() { chargingControlRequest.header("accept", "application/json"); chargingControlRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)"); chargingControlRequest.header("accept-language", "de"); - chargingControlRequest.header("Content-Type", CONTENT_TYPE_JSON_ENCODED); + chargingControlRequest.header("Content-Type", CONTENT_TYPE_JSON); - // String content = FileReader.readFileInString("src/test/resources/responses/charging-profile.json"); + // String content = FileReader.readFileInString("responses/charging-profile.json"); // logger.info("{}", content); - // ChargeProfile cpc = Converter.getGson().fromJson(content, ChargeProfile.class); + // ChargeProfile cpc = JsonStringDeserializer.deserializeString(content, ChargeProfile.class); // String contentTranfsorm = Converter.getGson().toJson(cpc); // String profile = "{chargingProfile:" + contentTranfsorm + "}"; // logger.info("{}", profile); @@ -325,15 +336,16 @@ public static String codeFromUrl(String encodedUrl) { @Test public void testJWTDeserialze() { - String accessTokenResponseStr = FileReader - .readFileInString("src/test/resources/responses/auth/auth_cn_login_pwd.json"); - ChinaTokenResponse cat = Converter.getGson().fromJson(accessTokenResponseStr, ChinaTokenResponse.class); + String accessTokenResponseStr = FileReader.fileToString("responses/auth/auth_cn_login_pwd.json"); + ChinaTokenResponse cat = JsonStringDeserializer.deserializeString(accessTokenResponseStr, + ChinaTokenResponse.class); // https://www.baeldung.com/java-jwt-token-decode String token = cat.data.accessToken; String[] chunks = token.split("\\."); String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1])); - ChinaTokenExpiration cte = Converter.getGson().fromJson(tokenJwtDecodeStr, ChinaTokenExpiration.class); + ChinaTokenExpiration cte = JsonStringDeserializer.deserializeString(tokenJwtDecodeStr, + ChinaTokenExpiration.class); Token t = new Token(); t.setToken(token); t.setType(cat.data.tokenType); @@ -350,12 +362,14 @@ public void testChina() { authHttpClient.start(); HttpClientFactory mockHCF = mock(HttpClientFactory.class); when(mockHCF.getCommonHttpClient()).thenReturn(authHttpClient); - MyBMWConfiguration config = new MyBMWConfiguration(); + MyBMWBridgeConfiguration config = new MyBMWBridgeConfiguration(); config.region = BimmerConstants.REGION_CHINA; config.userName = "Hello User"; config.password = "Hello Password"; - MyBMWProxy bmwProxy = new MyBMWProxy(mockHCF, config); - bmwProxy.updateTokenChina(); + MyBMWTokenController tokenHandler = new MyBMWTokenController(config, authHttpClient); + Token token = tokenHandler.getToken(); + assertNotNull(token); + assertNotNull(token.getBearerToken()); } catch (Exception e) { logger.warn("Exception: " + e.getMessage()); } @@ -363,8 +377,9 @@ public void testChina() { @Test public void testPublicKey() { - String publicKeyResponseStr = FileReader.readFileInString("src/test/resources/responses/auth/china-key.json"); - ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponseStr, ChinaPublicKeyResponse.class); + String publicKeyResponseStr = FileReader.fileToString("responses/auth/china-key.json"); + ChinaPublicKeyResponse pkr = JsonStringDeserializer.deserializeString(publicKeyResponseStr, + ChinaPublicKeyResponse.class); String publicKeyStr = pkr.data.value; String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "") .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "") @@ -394,12 +409,12 @@ public void testChinaToken() { String url = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA) + BimmerConstants.CHINA_PUBLIC_KEY; Request oauthQueryRequest = authHttpClient.newRequest(url); - oauthQueryRequest.header(X_USER_AGENT, + oauthQueryRequest.header(HEADER_X_USER_AGENT, String.format(BimmerConstants.BRAND_BMW, BimmerConstants.BRAND_BMW, BimmerConstants.REGION_ROW)); ContentResponse publicKeyResponse = oauthQueryRequest.send(); - ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponse.getContentAsString(), - ChinaPublicKeyResponse.class); + ChinaPublicKeyResponse pkr = JsonStringDeserializer + .deserializeString(publicKeyResponse.getContentAsString(), ChinaPublicKeyResponse.class); // https://stackoverflow.com/questions/11410770/load-rsa-public-key-from-file String publicKeyStr = pkr.data.value; diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java new file mode 100644 index 0000000000000..71478216f6b20 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; +import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.util.FileReader; + +import com.google.gson.Gson; + +/** + * This test checks if the file can be parsed correctly to the object. Additionally + * it can be used to check if there are properties in the files not mapped to the objects + * The output of Junit (at least in VSCode) shows the differences between the original file + * and the string generated by GSON to identify the gap. + * + * @author Martin Grassl - Initial contribution + */ +@NonNullByDefault +public class JsonStringDeserializerTest { + + Gson gson = new Gson(); + + void testGetChargeSessions() { + String content = FileReader.fileToString("responses/BEV/charging_sessions.json"); + ChargingSessionsContainer chargeSessionsContainer = JsonStringDeserializer.getChargingSessions(content); + assertNotNull(chargeSessionsContainer); + + // String jsonString = gson.toJson(chargeSessionsContainer); + // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); + } + + @Test + void testGetChargeStatistics() { + String content = FileReader.fileToString("responses/BEV/charging_statistics.json"); + ChargingStatisticsContainer chargeStatisticsContainer = JsonStringDeserializer.getChargingStatistics(content); + assertNotNull(chargeStatisticsContainer); + + // String jsonString = gson.toJson(chargeStatisticsContainer); + // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); + } + + @Test + void testGetExecutionStatus() { + String content = FileReader.fileToString("responses/MILD_HYBRID/remote_service_status.json"); + ExecutionStatusContainer executionStatusContainer = JsonStringDeserializer.getExecutionStatus(content); + assertNotNull(executionStatusContainer); + + // String jsonString = gson.toJson(executionStatusContainer); + // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); + } + + @Test + void testGetExecutionError() { + String content = FileReader.fileToString("responses/MILD_HYBRID/remote_service_error.json"); + ExecutionStatusContainer executionStatusContainer = JsonStringDeserializer.getExecutionStatus(content); + assertNotNull(executionStatusContainer); + + // String jsonString = gson.toJson(executionStatusContainer); + // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); + } + + @Test + void testGetVehicleBaseList() { + String content = FileReader.fileToString("responses/BEV/vehicles_base.json"); + List vehicleBases = JsonStringDeserializer.getVehicleBaseList(content); + assertNotNull(vehicleBases); + assertFalse(vehicleBases.isEmpty()); + assertEquals(1, vehicleBases.size()); + + // String jsonString = gson.toJson(vehicleBases); + // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); + } + + @Test + void testGetVehicleStateBEV() { + String content = FileReader.fileToString("responses/BEV/vehicles_state.json"); + VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(content); + assertNotNull(vehicleStateContainer); + + vehicleStateContainer.setRawStateJson(null); + String jsonString = gson.toJson(vehicleStateContainer); + assertNotNull(jsonString); + } + + @Test + void testGetVehicleStateMILDHYBRID() { + String content = FileReader.fileToString("responses/MILD_HYBRID/vehicles_state.json"); + VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(content); + assertNotNull(vehicleStateContainer); + + // String jsonString = gson.toJson(vehicleStateContainer); + // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); + } + + @Test + void testGetVehicleStatePHEV() { + String content = FileReader.fileToString("responses/PHEV/vehicles_state.json"); + VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(content); + assertNotNull(vehicleStateContainer); + + // String jsonString = gson.toJson(vehicleStateContainer); + // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); + } + + @Test + void testGetVehicleStateICE() { + String content = FileReader.fileToString("responses/ICE/vehicles_state.json"); + VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(content); + assertNotNull(vehicleStateContainer); + + // String jsonString = gson.toJson(vehicleStateContainer); + // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java new file mode 100644 index 0000000000000..8a306f5647f93 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; +import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; +import org.openhab.binding.mybmw.internal.util.FileReader; +import org.openhab.binding.mybmw.internal.utils.BimmerConstants; +import org.openhab.binding.mybmw.internal.utils.ImageProperties; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; + +/** + * this test tests the different MyBMWProxy request types (GET, POST) and their errors (SUCCESS, other) + * + * @author Martin Grassl - initial contribution + */ +@NonNullByDefault +public class MyBMWHttpProxyTest { + + private final Logger logger = LoggerFactory.getLogger(MyBMWHttpProxyTest.class); + + @BeforeEach + public void setupLogger() { + Logger root = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + + if ("debug".equals(System.getenv("LOG_LEVEL"))) { + ((ch.qos.logback.classic.Logger) root).setLevel(Level.DEBUG); + } else if ("trace".equals(System.getenv("LOG_LEVEL"))) { + ((ch.qos.logback.classic.Logger) root).setLevel(Level.TRACE); + } + + logger.trace("tracing enabled"); + logger.debug("debugging enabled"); + logger.info("info enabled"); + } + + @Test + void testWrongBrand() { + // test successful GET for vehicle state + String responseContent = FileReader.fileToString("responses/BEV/vehicles_state.json"); + MyBMWHttpProxy myBMWProxy = generateMyBmwProxy(200, responseContent); + + try { + myBMWProxy.requestVehicleState("testVin", "WRONG_BRAND"); + } catch (NetworkException e) { + assertEquals("Unknown Brand WRONG_BRAND", e.getMessage()); + } + } + + @Test + void testSuccessfulGet() { + // test successful GET for vehicle state + String responseContent = FileReader.fileToString("responses/BEV/vehicles_state.json"); + MyBMWHttpProxy myBMWProxy = generateMyBmwProxy(200, responseContent); + + try { + VehicleStateContainer vehicleStateContainer = myBMWProxy.requestVehicleState("testVin", + BimmerConstants.BRAND_BMW); + assertEquals(2686, vehicleStateContainer.getState().getCurrentMileage()); + } catch (NetworkException e) { + fail(e.toString()); + } + } + + @Test + void testErrorGet() { + // test successful GET for vehicle state + String responseContent = FileReader.fileToString("responses/BEV/vehicles_state.json"); + MyBMWHttpProxy myBMWProxy = generateMyBmwProxy(400, responseContent); + + try { + myBMWProxy.requestVehicleState("testVin", BimmerConstants.BRAND_BMW); + + fail("here an exception should be thrown"); + } catch (NetworkException e) { + assertEquals(400, e.getStatus()); + } + } + + @Test + void testSuccessfulPost() { + // test successful POST for remote service execution + String responseContent = FileReader.fileToString("responses/MILD_HYBRID/remote_service_call.json"); + MyBMWHttpProxy myBMWProxy = generateMyBmwProxy(200, responseContent); + + try { + ExecutionStatusContainer executionStatusContainer = myBMWProxy.executeRemoteServiceCall("testVin", + BimmerConstants.BRAND_BMW, RemoteService.LIGHT_FLASH); + assertNotNull(executionStatusContainer.getCreationTime()); + assertNotNull(executionStatusContainer.getEventId()); + assertEquals("", executionStatusContainer.getEventStatus()); + } catch (NetworkException e) { + fail(e.toString()); + } + } + + @Test + void testErrorPost() { + // test successful POST for remote service execution + String responseContent = FileReader.fileToString("responses/MILD_HYBRID/remote_service_call.json"); + MyBMWHttpProxy myBMWProxy = generateMyBmwProxy(400, responseContent); + + try { + myBMWProxy.executeRemoteServiceCall("testVin", BimmerConstants.BRAND_BMW, RemoteService.LIGHT_FLASH); + fail("here an exception should be thrown"); + } catch (NetworkException e) { + assertEquals(400, e.getStatus()); + } + } + + @Test + void testSuccessfulImage() { + // test successful POST for remote service execution + byte[] responseContent = FileReader.fileToByteArray("responses/MILD_HYBRID/340i_frontView.png"); + MyBMWHttpProxy myBMWProxy = generateMyBmwProxy(200, new String(responseContent)); + + try { + byte[] image = myBMWProxy.requestImage("testVin", BimmerConstants.BRAND_BMW, new ImageProperties()); + assertNotNull(image); + } catch (NetworkException e) { + fail(e.toString()); + } + } + + @Test + void testSuccessfulGetVehicles() { + HttpClientFactory httpClientFactoryMock = Mockito.mock(HttpClientFactory.class); + HttpClient httpClientMock = Mockito.mock(HttpClient.class); + Mockito.when(httpClientFactoryMock.getCommonHttpClient()).thenReturn(httpClientMock); + + MyBMWBridgeConfiguration myBMWBridgeConfiguration = new MyBMWBridgeConfiguration(); + + MyBMWHttpProxy myBMWProxyMock = Mockito + .spy(new MyBMWHttpProxy(httpClientFactoryMock, myBMWBridgeConfiguration)); + + String vehiclesBaseString = FileReader.fileToString("responses/BEV/vehicles_base.json"); + List baseVehicles = JsonStringDeserializer.getVehicleBaseList(vehiclesBaseString); + + String vehicleStateString = FileReader.fileToString("responses/BEV/vehicles_state.json"); + VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(vehicleStateString); + + try { + doReturn(baseVehicles).when(myBMWProxyMock).requestVehiclesBase(); + doReturn(vehicleStateContainer).when(myBMWProxyMock).requestVehicleState(anyString(), anyString()); + + List vehicles = myBMWProxyMock.requestVehicles(); + + logger.debug("found vehicles {}", vehicles.toString()); + + assertNotNull(vehicles); + assertEquals(1, vehicles.size()); + assertEquals("I20", vehicles.get(0).getVehicleBase().getAttributes().getBodyType()); + + } catch (NetworkException e) { + fail("vehicles not loaded properly", e); + } + } + + MyBMWHttpProxy generateMyBmwProxy(int statuscode, String responseContent) { + HttpClientFactory httpClientFactoryMock = Mockito.mock(HttpClientFactory.class); + HttpClient httpClientMock = Mockito.mock(HttpClient.class); + Request requestMock = Mockito.mock(Request.class); + Mockito.when(httpClientMock.newRequest(Mockito.anyString())).thenReturn(requestMock); + Mockito.when(httpClientMock.POST(Mockito.anyString())).thenReturn(requestMock); + MyBMWBridgeConfiguration myBMWBridgeConfiguration = new MyBMWBridgeConfiguration(); + Mockito.when(httpClientFactoryMock.getCommonHttpClient()).thenReturn(httpClientMock); + + ContentResponse responseMock = Mockito.mock(ContentResponse.class); + Mockito.when(responseMock.getStatus()).thenReturn(statuscode); + Mockito.when(responseMock.getContent()).thenReturn(responseContent.getBytes()); + Mockito.when(responseMock.getContentAsString()).thenReturn(responseContent); + try { + Mockito.when(requestMock.timeout(anyLong(), any())).thenReturn(requestMock); + Mockito.when(requestMock.send()).thenReturn(responseMock); + } catch (InterruptedException e1) { + logger.error(e1.getMessage(), e1); + } catch (TimeoutException e1) { + logger.error(e1.getMessage(), e1); + } catch (ExecutionException e1) { + logger.error(e1.getMessage(), e1); + } + + return new MyBMWHttpProxy(httpClientFactoryMock, myBMWBridgeConfiguration); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java new file mode 100644 index 0000000000000..62c0a245b5b4d --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; +import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; +import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; +import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; +import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; +import org.openhab.binding.mybmw.internal.handler.enums.ExecutionState; +import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; +import org.openhab.binding.mybmw.internal.utils.BimmerConstants; +import org.openhab.binding.mybmw.internal.utils.ImageProperties; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +import ch.qos.logback.classic.Level; + +/** + * this integration test runs only if the connected account is set via environment variables + * CONNECTED_USER + * CONNECTED_PASSWORD + * + * if you want to execute the tests, please set the env variables and remove the disabled annotation + * + * @author Martin Grassl - initial contribution + */ +@NonNullByDefault +public class MyBMWProxyBackendIT { + + private final Logger logger = LoggerFactory.getLogger(MyBMWProxyBackendIT.class); + + public MyBMWHttpProxy initializeProxy() { + String connectedUser = System.getenv("CONNECTED_USER"); + String connectedPassword = System.getenv("CONNECTED_PASSWORD"); + assertNotNull(connectedUser); + assertNotNull(connectedPassword); + + MyBMWBridgeConfiguration configuration = new MyBMWBridgeConfiguration(); + configuration.language = "en"; + configuration.region = BimmerConstants.REGION_ROW; + configuration.userName = connectedUser; + configuration.password = connectedPassword; + + return new MyBMWHttpProxy(new MyHttpClientFactory(), configuration); + } + + @BeforeEach + public void setupLogger() { + Logger root = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + + ((ch.qos.logback.classic.Logger) root).setLevel(Level.DEBUG); + + logger.trace("tracing enabled"); + logger.debug("debugging enabled"); + logger.info("info enabled"); + } + + @Test + public void testSequence() { + MyBMWHttpProxy myBMWProxy = initializeProxy(); + + // get list of vehicles + List<@NonNull VehicleBase> vehicles = null; + try { + vehicles = myBMWProxy.requestVehiclesBase(); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + + assertNotNull(vehicles); + assertEquals(2, vehicles.size()); + + for (VehicleBase vehicleBase : vehicles) { + assertNotNull(vehicleBase.getVin()); + assertNotNull(vehicleBase.getAttributes().getBrand()); + + // get image + try { + byte[] bmwImage = myBMWProxy.requestImage(vehicleBase.getVin(), vehicleBase.getAttributes().getBrand(), + new ImageProperties()); + + assertNotNull(bmwImage); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + + // get state + VehicleStateContainer vehicleState = null; + try { + vehicleState = myBMWProxy.requestVehicleState(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand()); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + assertNotNull(vehicleState); + + // get charge statistics -> only successful for electric vehicles + ChargingStatisticsContainer chargeStatisticsContainer = null; + try { + chargeStatisticsContainer = myBMWProxy.requestChargeStatistics(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand()); + assertNotNull(chargeStatisticsContainer); + } catch (NetworkException e) { + logger.trace("error: {}", e.toString()); + } + + ChargingSessionsContainer chargeSessionsContainer = null; + try { + chargeSessionsContainer = myBMWProxy.requestChargeSessions(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand()); + assertNotNull(chargeSessionsContainer); + } catch (NetworkException e) { + logger.trace("error: {}", e.toString()); + } + + ExecutionStatusContainer remoteExecutionResponse = null; + try { + remoteExecutionResponse = myBMWProxy.executeRemoteServiceCall(vehicleBase.getVin(), + vehicleBase.getAttributes().getBrand(), RemoteService.LIGHT_FLASH); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + + assertNotNull(remoteExecutionResponse); + logger.warn("{}", remoteExecutionResponse.toString()); + + ExecutionStatusContainer remoteExecutionStatusResponse = null; + try { + remoteExecutionStatusResponse = myBMWProxy.executeRemoteServiceStatusCall( + vehicleBase.getAttributes().getBrand(), remoteExecutionResponse.getEventId()); + + assertNotNull(remoteExecutionStatusResponse); + logger.warn("{}", remoteExecutionStatusResponse.toString()); + + int counter = 0; + while (!ExecutionState.EXECUTED.toString().equals(remoteExecutionStatusResponse.getEventStatus()) + && counter++ < 10) { + remoteExecutionStatusResponse = myBMWProxy.executeRemoteServiceStatusCall( + vehicleBase.getAttributes().getBrand(), remoteExecutionResponse.getEventId()); + logger.warn("{}", remoteExecutionStatusResponse.toString()); + + Thread.sleep(5000); + } + } catch (NetworkException e) { + fail(e.getReason(), e); + } catch (InterruptedException e) { + fail(e.getMessage(), e); + } + } + } + + @Test + @Disabled + public void testGetVehicles() { + MyBMWHttpProxy myBMWProxy = initializeProxy(); + + try { + List<@NonNull Vehicle> vehicles = myBMWProxy.requestVehicles(); + + logger.warn(ResponseContentAnonymizer.anonymizeResponseContent(new Gson().toJson(vehicles))); + assertNotNull(vehicles); + assertEquals(2, vehicles.size()); + } catch (NetworkException e) { + fail(e.getReason(), e); + } + } +} + +/** + * @author Martin Grassl - initial contribution + */ +@NonNullByDefault +class MyHttpClientFactory implements HttpClientFactory { + + private final Logger logger = LoggerFactory.getLogger(MyHttpClientFactory.class); + + @Override + public HttpClient createHttpClient(String consumerName) { + // Instantiate and configure the SslContextFactory + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + // Instantiate HttpClient with the SslContextFactory + HttpClient httpClient = new HttpClient(sslContextFactory); + + // Configure HttpClient, for example: + httpClient.setFollowRedirects(false); + + // Start HttpClient + try { + httpClient.start(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + + return httpClient; + } + + @Override + public HttpClient getCommonHttpClient() { + return createHttpClient("test"); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizerTest.java new file mode 100644 index 0000000000000..b1cc4206cfacf --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizerTest.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.handler.backend; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mybmw.internal.util.FileReader; + +/** + * + * checks if the response anonymization is successful + * + * @author Martin Grassl - initial contribution + */ +@NonNullByDefault +public class ResponseContentAnonymizerTest { + @Test + void testAnonymizeResponseContent() { + String content = FileReader.fileToString("responses/vehicles.json"); + String anonymous = ResponseContentAnonymizer.anonymizeResponseContent(content); + assertFalse(anonymous.contains("VIN1234567"), "VIN not deleted!"); + assertFalse(anonymous.contains("Testort"), "Location not deleted!"); + } + + @Test + void testAnonymizeRandomString() { + String content = "asdfiulsahföauifhnasdölfam,xöasiocjfsailfunsalifnsaölfkmasdäf.ifnvaskdfnvinlocationasdfiulsdanf"; + String anonymous = ResponseContentAnonymizer.anonymizeResponseContent(content); + assertEquals(content, anonymous); + } + + @Test + void testAnonymizeEmptyString() { + String content = ""; + String anonymous = ResponseContentAnonymizer.anonymizeResponseContent(content); + assertEquals(content, anonymous); + } + + @Test + void testAnonymizeNullString() { + String content = null; + String anonymous = ResponseContentAnonymizer.anonymizeResponseContent(content); + assertEquals("", anonymous); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/FileReader.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/FileReader.java index 559d7daf576fb..bb6abaf052264 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/FileReader.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/util/FileReader.java @@ -12,11 +12,12 @@ */ package org.openhab.binding.mybmw.internal.util; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import java.io.BufferedReader; -import java.io.FileInputStream; +import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -26,23 +27,49 @@ * The {@link FileReader} Helper Util to read test resource files * * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - added reading of image */ @NonNullByDefault public class FileReader { - public static String readFileInString(String filename) { - try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "UTF-8"));) { + /** + * reads a file into a string + * + * @param filename + * @return + */ + public static String fileToString(String filename) { + try (BufferedReader br = new BufferedReader( + new InputStreamReader(FileReader.class.getClassLoader().getResourceAsStream(filename), "UTF-8"))) { StringBuilder buf = new StringBuilder(); String sCurrentLine; while ((sCurrentLine = br.readLine()) != null) { buf.append(sCurrentLine); } - return buf.toString(); + return buf != null ? buf.toString() : ""; } catch (IOException e) { - // fail if file cannot be read - assertEquals(filename, Constants.EMPTY, "Read failute " + filename); + fail("Read failure " + filename, e); } return Constants.UNDEF; } + + /** + * reads a file into a byte[] + * + * @param filename + * @return + */ + public static byte[] fileToByteArray(String filename) { + File file = new File(filename); + byte[] bytes = new byte[(int) file.length()]; + + try (InputStream is = (FileReader.class.getClassLoader().getResourceAsStream(filename))) { + is.read(bytes); + } catch (IOException e) { + fail("Read failure " + filename, e); + } + + return bytes; + } } diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java new file mode 100644 index 0000000000000..c5d5c0db06a74 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * @author Martin Grassl - initial contribution + */ +@NonNullByDefault +public class ConverterTest { + @Test + void testToTitleCase() { + assertEquals("Closed", Converter.toTitleCase("CLOSED")); + assertEquals("Secured", Converter.toTitleCase("SECURED")); + assertEquals("Undef", Converter.toTitleCase(null)); + assertEquals("Undef", Converter.toTitleCase("")); + assertEquals("Secured", Converter.toTitleCase("SECURED")); + assertEquals("Secured", Converter.toTitleCase("SECURED")); + assertEquals("Test Data", Converter.toTitleCase("test_data")); + assertEquals("Test-Data", Converter.toTitleCase("test-data")); + assertEquals("Test Data", Converter.toTitleCase("test data")); + } + + @Test + void testDateConversion() { + State state = Converter.zonedToLocalDateTime(null); + assertTrue(state instanceof UnDefType); + state = Converter.zonedToLocalDateTime(""); + assertTrue(state instanceof UnDefType); + state = Converter.zonedToLocalDateTime("2023-01-18"); + assertTrue(state instanceof UnDefType); + state = Converter.zonedToLocalDateTime("2023-01-18T18:07:59.076Z"); + assertTrue(state instanceof DateTimeType); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java new file mode 100644 index 0000000000000..df90a6612363f --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mybmw.internal.utils; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; + +/** + * + * checks if the configuration checker works fine + * + * @author Bernd Weymann - Initial contribution + * @author Martin Grassl - renamed + */ +@NonNullByDefault +public class MyBMWConfigurationCheckerTest { + @Test + void testCheckConfiguration() { + MyBMWBridgeConfiguration cdc = new MyBMWBridgeConfiguration(); + assertFalse(MyBMWConfigurationChecker.checkConfiguration(cdc)); + cdc.userName = "a"; + assertFalse(MyBMWConfigurationChecker.checkConfiguration(cdc)); + cdc.password = "b"; + assertFalse(MyBMWConfigurationChecker.checkConfiguration(cdc)); + cdc.region = "c"; + assertFalse(MyBMWConfigurationChecker.checkConfiguration(cdc)); + cdc.region = BimmerConstants.REGION_NORTH_AMERICA; + assertTrue(MyBMWConfigurationChecker.checkConfiguration(cdc)); + cdc.region = BimmerConstants.REGION_ROW; + assertTrue(MyBMWConfigurationChecker.checkConfiguration(cdc)); + cdc.region = BimmerConstants.REGION_CHINA; + assertTrue(MyBMWConfigurationChecker.checkConfiguration(cdc)); + } +} diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/530e/vehicles.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/530e/vehicles.json deleted file mode 100644 index 2d05f4f6f28af..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/530e/vehicles.json +++ /dev/null @@ -1,379 +0,0 @@ -[{ - "vin": "anonymous", - "model": "530e", - "year": 2021, - "brand": "BMW", - "headUnit": "MGU", - "isLscSupported": true, - "driveTrain": "PLUGIN_HYBRID", - "puStep": "1121", - "iStep": "S15A-21-11-530", - "telematicsUnit": "ATM02", - "hmiVersion": "id7", - "bodyType": "G30", - "a4aType": "NOT_SUPPORTED", - "exFactoryPUStep": "1121", - "exFactoryILevel": "S15A-21-11-530", - "capabilities": { - "isRemoteServicesBookingRequired": false, - "isRemoteServicesActivationRequired": false, - "isRemoteHistorySupported": true, - "canRemoteHistoryBeDeleted": false, - "isChargingHistorySupported": true, - "isScanAndChargeSupported": true, - "isDCSContractManagementSupported": true, - "isBmwChargingSupported": true, - "isMiniChargingSupported": false, - "isChargeNowForBusinessSupported": true, - "isDataPrivacyEnabled": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingTargetSocEnable": false, - "isChargingLoudnessEnable": false, - "isChargingSettingsEnabled": false, - "isChargingHospitalityEnabled": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isCustomerEsimSupported": false, - "isCarSharingSupported": false, - "isEasyChargeSupported": false, - "lock": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "unlock": { - "isEnabled": true, - "isPinAuthenticationRequired": true, - "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "lights": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern." - }, - "horn": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern." - }, - "vehicleFinder": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern." - }, - "sendPoi": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern." - }, - "climateNow": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern." - } - }, - "properties": { - "lastUpdatedAt": "2022-01-06T15:59:07Z", - "inMotion": false, - "areDoorsLocked": false, - "originCountryISO": "DE", - "areDoorsClosed": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "trunk": "CLOSED", - "hood": "CLOSED" - }, - "isServiceRequired": false, - "fuelLevel": { - "value": 18, - "units": "LITERS" - }, - "chargingState": { - "chargePercentage": 60, - "state": "CHARGING", - "type": "NOT_AVAILABLE", - "isChargerConnected": true - }, - "combustionRange": { - "chargePercentage": 0, - "distance": { - "value": 251, - "units": "KILOMETERS" - } - }, - "combinedRange": { - "chargePercentage": 0, - "distance": { - "value": 251, - "units": "KILOMETERS" - } - }, - "electricRange": { - "chargePercentage": 0, - "distance": { - "value": 27, - "units": "KILOMETERS" - } - }, - "electricRangeAndStatus": { - "chargePercentage": 60, - "distance": { - "value": 27, - "units": "KILOMETERS" - } - }, - "checkControlMessages": [], - "serviceRequired": [ - { - "type": "OIL", - "status": "OK", - "dateTime": "2023-12-01T00:00:00.000Z", - "distance": { - "value": 30000, - "units": "KILOMETERS" - } - }, - { - "type": "VEHICLE_CHECK", - "status": "OK", - "dateTime": "2025-12-01T00:00:00.000Z", - "distance": { - "value": 60000, - "units": "KILOMETERS" - } - }, - { - "type": "BRAKE_FLUID", - "status": "OK", - "dateTime": "2024-12-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_TUV", - "status": "OK", - "dateTime": "2024-12-01T00:00:00.000Z" - } - ], - "vehicleLocation": { - "coordinates": { - "latitude": 1.234, - "longitude": 5.678 - }, - "address": { - "formatted": "anonymous" - }, - "heading": 270 - }, - "tires": { - "frontLeft": { - "status": { - "currentPressure": 240.0, - "localizedCurrentPressure": "2,4 bar", - "localizedTargetPressure": "2,4 bar", - "targetPressure": 240.0 - } - }, - "frontRight": { - "status": { - "currentPressure": 240.0, - "localizedCurrentPressure": "2,4 bar", - "localizedTargetPressure": "2,4 bar", - "targetPressure": 240.0 - } - }, - "rearLeft": { - "status": { - "currentPressure": 270.0, - "localizedCurrentPressure": "2,7 bar", - "localizedTargetPressure": "2,8 bar", - "targetPressure": 280.0 - } - }, - "rearRight": { - "status": { - "currentPressure": 270.0, - "localizedCurrentPressure": "2,7 bar", - "localizedTargetPressure": "2,8 bar", - "targetPressure": 280.0 - } - } - } - }, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "status": { - "lastUpdatedAt": "2022-01-06T15:59:07Z", - "currentMileage": { - "mileage": 589, - "units": "km", - "formattedMileage": "589" - }, - "issues": null, - "doorsGeneralState": "Entriegelt", - "checkControlMessagesGeneralState": "Keine Probleme", - "doorsAndWindows": [ - { - "iconId": 59737, - "title": "Verriegelungsstatus", - "state": "Entriegelt", - "criticalness": "nonCritical" - }, - { - "iconId": 59722, - "title": "Alle Türen", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59725, - "title": "Alle Fenster", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59706, - "title": "Frontklappe", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59704, - "title": "Gepäckraum", - "state": "Geschlossen", - "criticalness": "nonCritical" - } - ], - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60117, - "state": "OK", - "title": "Reifen", - "longDescription": "-" - }, - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Motoröl", - "longDescription": "-" - } - ], - "requiredServices": [ - { - "id": "Oil", - "title": "Motoröl", - "iconId": 60197, - "longDescription": "Nächster Service nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im Dezember 2023 oder in 30.000 km", - "criticalness": "nonCritical" - }, - { - "id": "VehicleCheck", - "title": "Fahrzeug-Check", - "iconId": 60215, - "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im Dezember 2025 oder in 60.000 km", - "criticalness": "nonCritical" - }, - { - "id": "BrakeFluid", - "title": "Bremsflüssigkeit", - "iconId": 60223, - "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.", - "subtitle": "Fällig im Dezember 2024", - "criticalness": "nonCritical" - }, - { - "id": "VehicleAdmissionTest", - "title": "Fahrzeuginspektion (HU)", - "iconId": 60111, - "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.", - "subtitle": "Fällig im Dezember 2024", - "criticalness": "nonCritical" - } - ], - "fuelIndicators": [ - { - "mainBarValue": 0, - "rangeUnits": "km", - "rangeValue": "251" - }, - { - "mainBarValue": 60, - "rangeUnits": "km", - "rangeValue": "27", - "levelUnits": "%", - "levelValue": "60" - }, - { - "mainBarValue": 42, - "rangeUnits": "km", - "rangeValue": "224", - "levelUnits": "%", - "levelValue": "42" - } - ], - "timestampMessage": "Aktualisiert vom Fahrzeug 6.1.2022 04:59 PM", - "chargingProfile": { - "reductionOfChargeCurrent": { - "start": { - "hour": 0, - "minute": 0 - }, - "end": { - "hour": 0, - "minute": 0 - } - }, - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingControlType": "weeklyPlanner", - "departureTimes": [ - { - "id": 1, - "action": "deactivate", - "timerWeekDays": [] - }, - { - "id": 2, - "action": "deactivate", - "timerWeekDays": [] - }, - { - "id": 3, - "action": "deactivate", - "timerWeekDays": [] - }, - { - "id": 4, - "action": "deactivate", - "timerWeekDays": [] - } - ], - "climatisationOn": true, - "chargingSettings": { - "targetSoc": 100, - "isAcCurrentLimitActive": false, - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION" - } - } - }, - "valid": false -} -] - diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/charging_sessions.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/charging_sessions.json new file mode 100644 index 0000000000000..2ece14b711162 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/charging_sessions.json @@ -0,0 +1,74 @@ +{ + "paginationInfo": null, + "chargingSessions": { + "total": "~ 142 kWh", + "numberOfSessions": "7", + "chargingListState": "HAS_SESSIONS", + "sessions": [ + { + "id": "2023-01-13T20:46:46Z_20735abd", + "title": "Vrijdag 21:46", + "subtitle": "Leopoldstrasse 30 • 2u 06min • -- EUR", + "energyCharged": "~ 22 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-01-12T17:39:51Z_20735abd", + "title": "Donderdag 18:39", + "subtitle": "Leopoldstrasse 30 • 3u 00min • -- EUR", + "energyCharged": "~ 31 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-01-08T16:16:13Z_20735abd", + "title": "08-01-2023 17:16", + "subtitle": "Leopoldstrasse 30 • 2u 15min • -- EUR", + "energyCharged": "~ 24 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-01-08T16:16:04.000Z_20735abd", + "title": "08-01-2023 17:16", + "subtitle": "Leopoldstrasse 30 • 0 min • -- EUR", + "energyCharged": "0 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-01-08T15:05:06Z_20735abd", + "title": "08-01-2023 16:05", + "subtitle": "Leopoldstrasse 30 • 2 min • -- EUR", + "energyCharged": "< 2 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-01-05T22:46:55Z_20735abd", + "title": "05-01-2023 23:46", + "subtitle": "Leopoldstrasse 30 • 2u 30min • -- EUR", + "energyCharged": "~ 27 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-01-03T17:08:29Z_20735abd", + "title": "03-01-2023 18:08", + "subtitle": "Leopoldstrasse 30 • 3u 34min • -- EUR", + "energyCharged": "~ 38 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + } + ], + "costsGroupedByCurrency": [ + "--" + ] + }, + "datePicker": { + "startDate": "2022-11-25T12:15:40Z", + "selectedDate": "2023-01-13T20:46:48Z", + "endDate": "2023-01-16T15:20:49Z" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/charging_statistics.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/charging_statistics.json new file mode 100644 index 0000000000000..645aa3a0b3630 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/charging_statistics.json @@ -0,0 +1,11 @@ +{ + "description": "januari 2023", + "optStateType": "OPT_IN_WITH_SESSIONS", + "statistics": { + "totalEnergyCharged": 142, + "totalEnergyChargedSemantics": "In totaal circa 142 kilowattuur geladen", + "symbol": "~", + "numberOfChargingSessions": 7, + "numberOfChargingSessionsSemantics": "7 laadprocessen" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/vehicles_base.json new file mode 100644 index 0000000000000..1c3b07c4ce62e --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/vehicles_base.json @@ -0,0 +1,48 @@ +[ + { + "vin": "anonymousBEV", + "mappingInfo": { + "isAssociated": true, + "isLmmEnabled": true, + "mappingStatus": "CONFIRMED", + "isPrimaryUser": true + }, + "appVehicleType": "CONNECTED", + "attributes": { + "lastFetched": "2023-01-16T15:41:26.664Z", + "model": "iX xDrive40", + "year": 2022, + "color": 4282345065, + "brand": "BMW_I", + "driveTrain": "ELECTRIC", + "headUnitType": "MGU", + "headUnitRaw": "HU_MGU", + "hmiVersion": "ID8", + "softwareVersionCurrent": { + "puStep": { + "month": 7, + "year": 22 + }, + "iStep": 545, + "seriesCluster": "I020" + }, + "softwareVersionExFactory": { + "puStep": { + "month": 7, + "year": 22 + }, + "iStep": 545, + "seriesCluster": "I020" + }, + "telematicsUnit": "WAVE11", + "bodyType": "I20", + "countryOfOrigin": "BE", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + } + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/vehicles_state.json new file mode 100644 index 0000000000000..5a60806b3573a --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV/vehicles_state.json @@ -0,0 +1,292 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2023-01-16T15:25:49.307Z", + "lastUpdatedAt": "2023-01-16T12:32:48Z", + "isLscSupported": true, + "range": 191, + "doorsState": { + "combinedSecurityState": "SECURED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED" + }, + "tireState": { + "frontLeft": { + "details": { + "identificationInProgress": false + }, + "status": { + "currentPressure": 230, + "targetPressure": 220, + "wearStatus": 0 + } + }, + "frontRight": { + "details": { + "identificationInProgress": false + }, + "status": { + "currentPressure": 230, + "targetPressure": 220, + "wearStatus": 0 + } + }, + "rearLeft": { + "details": { + "identificationInProgress": false + }, + "status": { + "currentPressure": 260, + "targetPressure": 260, + "wearStatus": 0 + } + }, + "rearRight": { + "details": { + "identificationInProgress": false + }, + "status": { + "currentPressure": 280, + "targetPressure": 260, + "wearStatus": 0 + } + } + }, + "location": { + "coordinates": { + "latitude": 1.1, + "longitude": 2.2 + }, + "address": { + "formatted": "anonymousAddress" + }, + "heading": -1 + }, + "currentMileage": 2686, + "climateControlState": { + "activity": "INACTIVE" + }, + "requiredServices": [ + { + "dateTime": "2024-11-01T00:00:00.000Z", + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Volgende vervanging uiterlijk op het aangegeven tijdstip." + }, + { + "dateTime": "2024-11-01T00:00:00.000Z", + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Volgende visuele controle op de aangegeven datum of na afloop van de evt. aangegeven afstand." + }, + { + "type": "TIRE_WEAR_REAR", + "status": "OK" + }, + { + "type": "TIRE_WEAR_FRONT", + "status": "OK" + } + ], + "checkControlMessages": [ + { + "type": "TIRE_PRESSURE", + "severity": "LOW" + } + ], + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "reductionOfChargeCurrent": { + "start": { + "hour": 21, + "minute": 0 + }, + "end": { + "hour": 6, + "minute": 0 + } + }, + "chargingMode": "DELAYED_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "departureTimes": [ + { + "id": 1, + "timeStamp": { + "hour": 7, + "minute": 0 + }, + "action": "ACTIVATE", + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "id": 2, + "timeStamp": { + "hour": 23, + "minute": 59 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 3, + "timeStamp": { + "hour": 23, + "minute": 59 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 4, + "timeStamp": { + "hour": 7, + "minute": 30 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + } + ], + "climatisationOn": false, + "chargingSettings": { + "targetSoc": 80, + "acCurrentLimit": 32, + "idcc": "AUTOMATIC_INTELLIGENT", + "hospitality": "HOSP_INACTIVE", + "isAcCurrentLimitActive": false + } + }, + "electricChargingState": { + "chargingLevelPercent": 46, + "remainingChargingMinutes": 178, + "range": 159, + "isChargerConnected": true, + "chargingConnectionType": "UNKNOWN", + "chargingStatus": "CHARGING", + "chargingTarget": 80 + }, + "combustionFuelLevel": { + "range": 191 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "isDeepSleepModeActive": false, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + }, + "capabilities": { + "a4aType": "NOT_SUPPORTED", + "climateNow": true, + "climateFunction": "AIR_CONDITIONING", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": true, + "isChargingLoudnessEnabled": true, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": true, + "isChargingSettingsEnabled": true, + "isChargingTargetSocEnabled": true, + "isCustomerEsimSupported": true, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": true, + "isEasyChargeEnabled": false, + "isMiniChargingSupported": false, + "isEvGoChargingSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteEngineStartSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": true, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remote360": true, + "remoteSoftwareUpgrade": true, + "sendPoi": true, + "surroundViewRecorder": true, + "unlock": true, + "vehicleFinder": true, + "vehicleStateSource": "LAST_STATE_CALL", + "isRemoteHistorySupported": true, + "isWifiHotspotServiceSupported": false, + "isNonLscFeatureEnabled": false, + "isSustainabilitySupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "checkSustainabilityDPP": false, + "specialThemeSupport": [], + "isRemoteParkingSupported": false, + "remoteChargingCommands": { + "chargingControl": [ + "START", + "STOP" + ], + "plugControl": [ + "NOT_SUPPORTED" + ], + "flapControl": [ + "COUPLE_FLAP", + "DECOUPLE_FLAP" + ] + }, + "isClimateTimerWeeklyActive": false, + "digitalKey": { + "bookedServicePackage": "SMACC_2_UWB", + "readerGraphics": "000200000001", + "state": "ACTIVATED" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV2/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV2/vehicles_state.json new file mode 100644 index 0000000000000..9c947cdb3cb0d --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV2/vehicles_state.json @@ -0,0 +1,154 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2023-01-18T18:07:59.076Z", + "lastUpdatedAt": "2023-01-18T17:50:01Z", + "isLscSupported": true, + "range": 101, + "doorsState": { + "combinedSecurityState": "SECURED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "rightFront": "CLOSED", + "combinedState": "CLOSED" + }, + "roofState": { + "roofState": "CLOSED", + "roofStateType": "SUN_ROOF" + }, + "location": { + "coordinates": { + "latitude": 1.1, + "longitude": 2.2 + }, + "address": { + "formatted": "anonymousAddress" + }, + "heading": -1 + }, + "currentMileage": 129898, + "requiredServices": [ + { + "dateTime": "2024-07-01T00:00:00.000Z", + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Next service due by the specified date." + }, + { + "dateTime": "2024-07-01T00:00:00.000Z", + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Next vehicle check due after the specified distance or date." + } + ], + "checkControlMessages": [], + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "reductionOfChargeCurrent": { + "start": { + "hour": 0, + "minute": 0 + }, + "end": { + "hour": 8, + "minute": 0 + } + }, + "chargingMode": "IMMEDIATE_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "departureTimes": [ + { + "id": 1, + "timeStamp": { + "hour": 8, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 2, + "timeStamp": { + "hour": 3, + "minute": 10 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 3, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 4, + "action": "DEACTIVATE", + "timerWeekDays": [] + } + ], + "climatisationOn": false, + "chargingSettings": { + "targetSoc": 100, + "idcc": "NO_ACTION", + "hospitality": "NO_ACTION" + } + }, + "electricChargingState": { + "chargingLevelPercent": 73, + "range": 101, + "isChargerConnected": false, + "chargingConnectionType": "CONDUCTIVE", + "chargingStatus": "INVALID", + "chargingTarget": 100 + }, + "combustionFuelLevel": null, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/charging_sessions.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/charging_sessions.json new file mode 100644 index 0000000000000..41dd429add804 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/charging_sessions.json @@ -0,0 +1,50 @@ +{ + "paginationInfo": {}, + "chargingSessions": { + "total": "~ 78 kWh", + "numberOfSessions": "4", + "chargingListState": "HAS_SESSIONS", + "sessions": [ + { + "id": "2023-01-21T09:26:09Z_c700db02", + "title": "Today 10:26", + "subtitle": "anonymousAddress • 36 min • – EUR", + "energyCharged": "~ 7 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-01-17T10:33:37Z_c700db02", + "title": "Tuesday 11:33", + "subtitle": "anonymousAddress • 3h 39min • – EUR", + "energyCharged": "~ 39 kWh", + "sessionStatus": "FINISHED", + "isPublic": true + }, + { + "id": "2023-01-05T14:07:49Z_c700db02", + "title": "1/5/2023 15:07", + "subtitle": "anonymousAddress • 52 min • – EUR", + "energyCharged": "~ 9 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-01-01T20:00:27Z_c700db02", + "title": "1/1/2023 21:00", + "subtitle": "anonymousAddress • 2h 38min • ~ 10,27 EUR", + "energyCharged": "~ 23 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + } + ], + "costsGroupedByCurrency": [ + "~10.27 EUR" + ] + }, + "datePicker": { + "startDate": "2022-06-13T10:29:32Z", + "selectedDate": "2023-01-21T09:26:09Z", + "endDate": "2023-01-21T18:57:40Z" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/charging_statistics.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/charging_statistics.json new file mode 100644 index 0000000000000..f9413c30d2492 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/charging_statistics.json @@ -0,0 +1,11 @@ +{ + "description": "January 2023", + "optStateType": "OPT_IN_WITH_SESSIONS", + "statistics": { + "totalEnergyCharged": 78, + "totalEnergyChargedSemantics": "Charged a total of approximately 78 kilowatt-hours", + "symbol": "~", + "numberOfChargingSessions": 4, + "numberOfChargingSessionsSemantics": "4 charging sessions" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/vehicles_base.json new file mode 100644 index 0000000000000..d4ddb4cd55866 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/vehicles_base.json @@ -0,0 +1,48 @@ +[ + { + "vin": "anonymousBEV3", + "mappingInfo": { + "isAssociated": true, + "isLmmEnabled": false, + "mappingStatus": "CONFIRMED", + "isPrimaryUser": true + }, + "appVehicleType": "CONNECTED", + "attributes": { + "lastFetched": "2023-01-21T18: 57: 39.114Z", + "model": "iX3 M Sport", + "year": 2022, + "color": 4280231501, + "brand": "BMW", + "driveTrain": "ELECTRIC", + "headUnitType": "MGU", + "headUnitRaw": "HU_MGU", + "hmiVersion": "ID7", + "softwareVersionCurrent": { + "puStep": { + "month": 3, + "year": 22 + }, + "iStep": 552, + "seriesCluster": "S15C" + }, + "softwareVersionExFactory": { + "puStep": { + "month": 11, + "year": 21 + }, + "iStep": 564, + "seriesCluster": "S15C" + }, + "telematicsUnit": "ATM2", + "bodyType": "G08", + "countryOfOrigin": "AT", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide: ///open", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + } + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/vehicles_state.json new file mode 100644 index 0000000000000..e284d44d121f2 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV3/vehicles_state.json @@ -0,0 +1,248 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2023-01-21T18:57:39.524Z", + "lastUpdatedAt": "2023-01-21T11:50:52Z", + "isLscSupported": true, + "range": 208, + "doorsState": { + "combinedSecurityState": "SECURED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED" + }, + "roofState": { + "roofState": "CLOSED", + "roofStateType": "SUN_ROOF" + }, + "tireState": { + "frontLeft": { + "status": { + "currentPressure": 250 + } + }, + "frontRight": { + "status": { + "currentPressure": 250 + } + }, + "rearLeft": { + "status": { + "currentPressure": 280 + } + }, + "rearRight": { + "status": { + "currentPressure": 290 + } + } + }, + "location": { + "coordinates": { + "latitude": 1.1, + "longitude": 2.2 + }, + "address": { + "formatted": "anonymousAddress" + }, + "heading": -1 + }, + "currentMileage": 18289, + "climateControlState": { + "activity": "INACTIVE" + }, + "requiredServices": [ + { + "dateTime": "2024-06-01T00:00:00.000Z", + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Next service due by the specified date." + }, + { + "dateTime": "2024-06-01T00:00:00.000Z", + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Next visual inspection due by specified date or, if shown, when stated distance has been reached." + }, + { + "dateTime": "2025-06-01T00:00:00.000Z", + "type": "VEHICLE_TUV", + "status": "OK", + "description": "Next state inspection due by the specified date." + } + ], + "checkControlMessages": [ + { + "type": "TIRE_PRESSURE", + "severity": "LOW" + } + ], + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "reductionOfChargeCurrent": { + "start": { + "hour": 0, + "minute": 0 + }, + "end": { + "hour": 0, + "minute": 0 + } + }, + "chargingMode": "IMMEDIATE_CHARGING", + "chargingPreference": "NO_PRESELECTION", + "departureTimes": [ + { + "id": 1, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 2, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 3, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 4, + "action": "DEACTIVATE", + "timerWeekDays": [] + } + ], + "climatisationOn": false, + "chargingSettings": { + "targetSoc": 90, + "idcc": "NO_ACTION", + "hospitality": "NO_ACTION" + } + }, + "electricChargingState": { + "chargingLevelPercent": 76, + "remainingChargingMinutes": 843, + "range": 208, + "isChargerConnected": false, + "chargingConnectionType": "UNKNOWN", + "chargingStatus": "INVALID", + "chargingTarget": 90 + }, + "combustionFuelLevel": { + "range": 208 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "isDeepSleepModeActive": false, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + }, + "capabilities": { + "a4aType": "NOT_SUPPORTED", + "climateNow": true, + "climateFunction": "AIR_CONDITIONING", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": true, + "isEasyChargeEnabled": false, + "isMiniChargingSupported": false, + "isEvGoChargingSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteEngineStartSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteSoftwareUpgrade": true, + "sendPoi": true, + "speechThirdPartyAlexa": true, + "speechThirdPartyAlexaSDK": false, + "unlock": true, + "vehicleFinder": true, + "vehicleStateSource": "LAST_STATE_CALL", + "isRemoteHistorySupported": true, + "isWifiHotspotServiceSupported": true, + "isNonLscFeatureEnabled": false, + "isSustainabilitySupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "checkSustainabilityDPP": false, + "specialThemeSupport": [], + "isRemoteParkingSupported": false, + "remoteChargingCommands": {}, + "isClimateTimerWeeklyActive": false, + "digitalKey": { + "bookedServicePackage": "SMACC_1_5", + "readerGraphics": "000200000000", + "state": "ACTIVATED" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV4/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV4/vehicles_base.json new file mode 100644 index 0000000000000..1925452de0e65 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV4/vehicles_base.json @@ -0,0 +1,50 @@ +[ + { + "appVehicleType": "DEMO", + "attributes": { + "a4aType": "NOT_SUPPORTED", + "bodyType": "G26", + "brand": "BMW", + "color": 4284245350, + "countryOfOrigin": "DE", + "driveTrain": "ELECTRIC", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + }, + "headUnitRaw": "HU_MGU", + "headUnitType": "MGU", + "hmiVersion": "ID8", + "lastFetched": "2023-01-04T14:57:06.019Z", + "model": "i4 eDrive40", + "softwareVersionCurrent": { + "iStep": 470, + "puStep": { + "month": 11, + "year": 21 + }, + "seriesCluster": "G026" + }, + "softwareVersionExFactory": { + "iStep": 470, + "puStep": { + "month": 11, + "year": 21 + }, + "seriesCluster": "G026" + }, + "telematicsUnit": "WAVE01", + "year": 2021 + }, + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "isPrimaryUser": true, + "lmmStatusReasons": [], + "mappingStatus": "CONFIRMED" + }, + "vin": "anonymousBEV4" + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV4/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV4/vehicles_state.json new file mode 100644 index 0000000000000..6d7e1cb3d9cb6 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV4/vehicles_state.json @@ -0,0 +1,311 @@ +{ + "capabilities": { + "a4aType": "NOT_SUPPORTED", + "checkSustainabilityDPP": false, + "climateFunction": "AIR_CONDITIONING", + "climateNow": true, + "digitalKey": { + "bookedServicePackage": "SMACC_1_5", + "readerGraphics": "readerGraphics", + "state": "ACTIVATED" + }, + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": true, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": true, + "isChargingLoudnessEnabled": true, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": true, + "isChargingSettingsEnabled": true, + "isChargingTargetSocEnabled": true, + "isClimateTimerWeeklyActive": false, + "isCustomerEsimSupported": true, + "isDCSContractManagementSupported": true, + "isDataPrivacyEnabled": false, + "isEasyChargeEnabled": true, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": true, + "isSustainabilityAccumulatedViewEnabled": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remote360": true, + "remoteSoftwareUpgrade": true, + "sendPoi": true, + "specialThemeSupport": [], + "speechThirdPartyAlexa": false, + "speechThirdPartyAlexaSDK": false, + "unlock": true, + "vehicleFinder": true, + "vehicleStateSource": "LAST_STATE_CALL" + }, + "state": { + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "chargingMode": "IMMEDIATE_CHARGING", + "chargingPreference": "NO_PRESELECTION", + "chargingSettings": { + "acCurrentLimit": 16, + "hospitality": "NO_ACTION", + "idcc": "UNLIMITED_LOUD", + "targetSoc": 80 + }, + "departureTimes": [ + { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "timerWeekDays": [] + }, + { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "timerWeekDays": [] + }, + { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "timerWeekDays": [] + }, + { + "action": "DEACTIVATE", + "id": 4, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "timerWeekDays": [] + } + ] + }, + "checkControlMessages": [ + { + "severity": "LOW", + "type": "TIRE_PRESSURE" + } + ], + "climateControlState": { + "activity": "STANDBY" + }, + "climateTimers": [ + { + "departureTime": { + "hour": 0, + "minute": 0 + }, + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "departureTime": { + "hour": 0, + "minute": 0 + }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "departureTime": { + "hour": 0, + "minute": 0 + }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [] + } + ], + "combustionFuelLevel": {}, + "currentMileage": 1121, + "doorsState": { + "combinedSecurityState": "LOCKED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "trunk": "CLOSED" + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "electricChargingState": { + "chargingConnectionType": "UNKNOWN", + "chargingLevelPercent": 80, + "chargingStatus": "INVALID", + "chargingTarget": 80, + "isChargerConnected": false, + "range": 472, + "remainingChargingMinutes": 10 + }, + "isLeftSteering": true, + "isLscSupported": true, + "lastFetched": "2023-01-04T14:57:06.386Z", + "lastUpdatedAt": "2023-01-04T14:57:06.407Z", + "location": { + "address": { + "formatted": "Am Olympiapark 1, 80809 München" + }, + "coordinates": { + "latitude": 48.177334, + "longitude": 11.556274 + }, + "heading": 180 + }, + "range": 472, + "requiredServices": [ + { + "dateTime": "2024-12-01T00:00:00.000Z", + "description": "", + "mileage": 50000, + "status": "OK", + "type": "BRAKE_FLUID" + }, + { + "dateTime": "2024-12-01T00:00:00.000Z", + "description": "", + "mileage": 50000, + "status": "OK", + "type": "VEHICLE_TUV" + }, + { + "dateTime": "2024-12-01T00:00:00.000Z", + "description": "", + "mileage": 50000, + "status": "OK", + "type": "VEHICLE_CHECK" + }, + { + "status": "OK", + "type": "TIRE_WEAR_REAR" + }, + { + "status": "OK", + "type": "TIRE_WEAR_FRONT" + } + ], + "tireState": { + "frontLeft": { + "details": { + "dimension": "225/35 R20 90Y XL", + "isOptimizedForOemBmw": true, + "manufacturer": "Pirelli", + "manufacturingWeek": 4021, + "mountingDate": "2022-03-07T00:00:00.000Z", + "partNumber": "2461756", + "season": 2, + "speedClassification": { + "atLeast": false, + "speedRating": 300 + }, + "treadDesign": "P-ZERO" + }, + "status": { + "currentPressure": 241, + "pressureStatus": 0, + "targetPressure": 269, + "wearStatus": 0 + } + }, + "frontRight": { + "details": { + "dimension": "225/35 R20 90Y XL", + "isOptimizedForOemBmw": true, + "manufacturer": "Pirelli", + "manufacturingWeek": 2419, + "mountingDate": "2022-03-07T00:00:00.000Z", + "partNumber": "2461756", + "season": 2, + "speedClassification": { + "atLeast": false, + "speedRating": 300 + }, + "treadDesign": "P-ZERO" + }, + "status": { + "currentPressure": 255, + "pressureStatus": 0, + "targetPressure": 269, + "wearStatus": 0 + } + }, + "rearLeft": { + "details": { + "dimension": "255/30 R20 92Y XL", + "isOptimizedForOemBmw": true, + "manufacturer": "Pirelli", + "manufacturingWeek": 1219, + "mountingDate": "2022-03-07T00:00:00.000Z", + "partNumber": "2461757", + "season": 2, + "speedClassification": { + "atLeast": false, + "speedRating": 300 + }, + "treadDesign": "P-ZERO" + }, + "status": { + "currentPressure": 324, + "pressureStatus": 0, + "targetPressure": 303, + "wearStatus": 0 + } + }, + "rearRight": { + "details": { + "dimension": "255/30 R20 92Y XL", + "isOptimizedForOemBmw": true, + "manufacturer": "Pirelli", + "manufacturingWeek": 1219, + "mountingDate": "2022-03-07T00:00:00.000Z", + "partNumber": "2461757", + "season": 2, + "speedClassification": { + "atLeast": false, + "speedRating": 300 + }, + "treadDesign": "P-ZERO" + }, + "status": { + "currentPressure": 331, + "pressureStatus": 0, + "targetPressure": 303, + "wearStatus": 0 + } + } + }, + "windowsState": { + "combinedState": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV5/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV5/vehicles_base.json new file mode 100644 index 0000000000000..eaea75f9de2c5 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV5/vehicles_base.json @@ -0,0 +1,50 @@ +[ + { + "appVehicleType": "DEMO", + "attributes": { + "a4aType": "BLUETOOTH", + "bodyType": "G70", + "brand": "BMW", + "color": 4284900182, + "countryOfOrigin": "DE", + "driveTrain": "ELECTRIC", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + }, + "headUnitRaw": "MGU_02_L", + "headUnitType": "MGU", + "hmiVersion": "ID8", + "lastFetched": "2023-01-04T15:03:07.150Z", + "model": "i7 xDrive60", + "softwareVersionCurrent": { + "iStep": 505, + "puStep": { + "month": 7, + "year": 22 + }, + "seriesCluster": "G070" + }, + "softwareVersionExFactory": { + "iStep": 450, + "puStep": { + "month": 7, + "year": 22 + }, + "seriesCluster": "G070" + }, + "telematicsUnit": "WAVE01", + "year": 2022 + }, + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "isPrimaryUser": true, + "lmmStatusReasons": [], + "mappingStatus": "CONFIRMED" + }, + "vin": "anonymousBEV5" + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV5/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV5/vehicles_state.json new file mode 100644 index 0000000000000..8c18830a0c740 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/BEV5/vehicles_state.json @@ -0,0 +1,319 @@ +{ + "capabilities": { + "a4aType": "BLUETOOTH", + "checkSustainabilityDPP": false, + "climateFunction": "AIR_CONDITIONING", + "climateNow": true, + "digitalKey": { + "bookedServicePackage": "SMACC_2_UWB", + "readerGraphics": "readerGraphics", + "state": "ACTIVATED" + }, + "horn": true, + "inCarCamera": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": true, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": true, + "isChargingLoudnessEnabled": true, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": true, + "isChargingSettingsEnabled": true, + "isChargingTargetSocEnabled": true, + "isClimateTimerWeeklyActive": false, + "isCustomerEsimSupported": true, + "isDCSContractManagementSupported": true, + "isDataPrivacyEnabled": false, + "isEasyChargeEnabled": true, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": true, + "isSustainabilityAccumulatedViewEnabled": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remote360": true, + "remoteSoftwareUpgrade": true, + "sendPoi": true, + "specialThemeSupport": [], + "speechThirdPartyAlexa": false, + "speechThirdPartyAlexaSDK": false, + "surroundViewRecorder": true, + "unlock": true, + "vehicleFinder": true, + "vehicleStateSource": "LAST_STATE_CALL" + }, + "state": { + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "chargingMode": "IMMEDIATE_CHARGING", + "chargingPreference": "NO_PRESELECTION", + "chargingSettings": { + "acCurrentLimit": 16, + "hospitality": "NO_ACTION", + "idcc": "UNLIMITED_LOUD", + "targetSoc": 80 + }, + "departureTimes": [ + { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "timerWeekDays": [] + }, + { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "timerWeekDays": [] + }, + { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "timerWeekDays": [] + }, + { + "action": "DEACTIVATE", + "id": 4, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "timerWeekDays": [] + } + ] + }, + "checkControlMessages": [ + { + "severity": "LOW", + "type": "TIRE_PRESSURE" + } + ], + "climateControlState": { + "activity": "INACTIVE" + }, + "climateTimers": [ + { + "departureTime": { + "hour": 0, + "minute": 0 + }, + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "departureTime": { + "hour": 0, + "minute": 0 + }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "departureTime": { + "hour": 0, + "minute": 0 + }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [] + } + ], + "combustionFuelLevel": { + "remainingFuelPercent": 10 + }, + "currentMileage": 1121, + "doorsState": { + "combinedSecurityState": "LOCKED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "trunk": "CLOSED" + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "electricChargingState": { + "chargingConnectionType": "UNKNOWN", + "chargingLevelPercent": 70, + "chargingStatus": "CHARGING", + "chargingTarget": 80, + "isChargerConnected": true, + "range": 340, + "remainingChargingMinutes": 10 + }, + "isLeftSteering": true, + "isLscSupported": true, + "lastFetched": "2023-01-04T14:57:06.371Z", + "lastUpdatedAt": "2023-01-04T14:57:06.383Z", + "location": { + "address": { + "formatted": "Am Olympiapark 1, 80809 München" + }, + "coordinates": { + "latitude": 48.177334, + "longitude": 11.556274 + }, + "heading": 180 + }, + "range": 340, + "requiredServices": [ + { + "dateTime": "2024-12-01T00:00:00.000Z", + "description": "", + "mileage": 50000, + "status": "OK", + "type": "BRAKE_FLUID" + }, + { + "dateTime": "2024-12-01T00:00:00.000Z", + "description": "", + "mileage": 50000, + "status": "OK", + "type": "VEHICLE_TUV" + }, + { + "dateTime": "2024-12-01T00:00:00.000Z", + "description": "", + "mileage": 50000, + "status": "OK", + "type": "VEHICLE_CHECK" + }, + { + "status": "OK", + "type": "TIRE_WEAR_REAR" + }, + { + "status": "OK", + "type": "TIRE_WEAR_FRONT" + } + ], + "roofState": { + "roofState": "CLOSED", + "roofStateType": "SUN_ROOF" + }, + "tireState": { + "frontLeft": { + "details": { + "dimension": "275/40 R22 107Y XL", + "isOptimizedForOemBmw": true, + "manufacturer": "Pirelli", + "manufacturingWeek": 4021, + "mountingDate": "2022-04-20T00:00:00.000Z", + "partNumber": "5A401A1", + "season": 2, + "speedClassification": { + "atLeast": false, + "speedRating": 300 + }, + "treadDesign": "P-ZERO" + }, + "status": { + "currentPressure": 241, + "pressureStatus": 0, + "targetPressure": 241, + "wearStatus": 0 + } + }, + "frontRight": { + "details": { + "dimension": "275/40 R22 107Y XL", + "isOptimizedForOemBmw": true, + "manufacturer": "Pirelli", + "manufacturingWeek": 4021, + "mountingDate": "2022-04-20T00:00:00.000Z", + "partNumber": "5A401A1", + "season": 2, + "speedClassification": { + "atLeast": false, + "speedRating": 300 + }, + "treadDesign": "P-ZERO" + }, + "status": { + "currentPressure": 241, + "pressureStatus": 0, + "targetPressure": 241, + "wearStatus": 0 + } + }, + "rearLeft": { + "details": { + "dimension": "275/40 R22 107Y XL", + "isOptimizedForOemBmw": true, + "manufacturer": "Pirelli", + "manufacturingWeek": 4021, + "mountingDate": "2022-04-20T00:00:00.000Z", + "partNumber": "5A401A1", + "season": 2, + "speedClassification": { + "atLeast": false, + "speedRating": 300 + }, + "treadDesign": "P-ZERO" + }, + "status": { + "currentPressure": 261, + "pressureStatus": 0, + "targetPressure": 269, + "wearStatus": 0 + } + }, + "rearRight": { + "details": { + "dimension": "275/40 R22 107Y XL", + "isOptimizedForOemBmw": true, + "manufacturer": "Pirelli", + "manufacturingWeek": 4021, + "mountingDate": "2022-04-20T00:00:00.000Z", + "partNumber": "5A401A1", + "season": 2, + "speedClassification": { + "atLeast": false, + "speedRating": 300 + }, + "treadDesign": "P-ZERO" + }, + "status": { + "currentPressure": 269, + "pressureStatus": 0, + "targetPressure": 269, + "wearStatus": 0 + } + } + }, + "windowsState": { + "combinedState": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F11/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F11/vehicles_v2_bmw_0.json deleted file mode 100644 index b0a48f13dd800..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F11/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,279 +0,0 @@ -[ - { - "a4aType": "NOT_SUPPORTED", - "bodyType": "F11", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Ventilation" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "climateTimer": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true, - "page": { - "description": "By setting a start time you let the vehicle know when you plan to use it.", - "primaryButtonText": "SEND TO VEHICLE", - "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE", - "subtitle": "Set start time", - "title": "Ventilation timer" - }, - "tile": { - "description": "Plan start time", - "iconId": 59774, - "title": "Ventilation timer" - } - }, - "isBmwChargingSupported": false, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": false, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": false, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": false, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": false, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "NOT_CAPABLE" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": false, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [], - "driveTrain": "COMBUSTION", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "F010-12-11-503", - "exFactoryPUStep": "1112", - "headUnit": "ID5", - "hmiVersion": "ID4", - "iStep": "F010-12-11-503", - "isLscSupported": false, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "530d", - "properties": { - "checkControlMessages": [], - "climateControl": {}, - "doorsAndWindows": { - "doors": {}, - "windows": {} - }, - "fuelLevel": { - "units": "LITERS", - "value": 24 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-03-10T08:02:08Z", - "originCountryISO": "GB", - "serviceRequired": [ - { - "dateTime": "2022-10-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2022-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 25000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2024-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 60000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - } - ] - }, - "puStep": "1112", - "status": { - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - }, - { - "criticalness": "semiCritical", - "iconId": 60217, - "id": "229", - "longDescription": "Charge by driving for longer periods or use external charger. Functions requiring battery will be switched off.", - "state": "Medium", - "title": "Battery discharged: Start engine" - }, - { - "criticalness": "nonCritical", - "iconId": 60217, - "id": "50", - "longDescription": "System unable to monitor tire pressure. Check tire pressures manually. Continued driving possible. Consult service center.", - "state": "Low", - "title": "Flat Tire Monitor (FTM) inactive" - } - ], - "checkControlMessagesGeneralState": "Multiple Issues", - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59726, - "state": "Unknown", - "title": "All doors" - }, - { - "criticalness": "nonCritical", - "iconId": 59701, - "state": "Unknown", - "title": "Left front window" - }, - { - "criticalness": "nonCritical", - "iconId": 59700, - "state": "Unknown", - "title": "Right front window" - }, - { - "criticalness": "nonCritical", - "iconId": 59703, - "state": "Unknown", - "title": "Left rear window" - }, - { - "criticalness": "nonCritical", - "iconId": 59702, - "state": "Unknown", - "title": "Right rear window" - }, - { - "criticalness": "nonCritical", - "iconId": 59721, - "state": "Unknown", - "title": "Back window" - } - ], - "doorsGeneralState": "Unknown", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": true, - "levelIconId": 59682, - "levelUnits": "l", - "levelValue": "24", - "mainBarValue": 0, - "rangeIconId": 59681, - "rangeUnits": "mi", - "rangeValue": "- -", - "secondaryBarValue": 0, - "showsBar": false - } - ], - "lastUpdatedAt": "2021-03-10T08:02:08Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in October 2022", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in October 2022 or 15534 mi", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in October 2024 or 37282 mi", - "title": "Vehicle check" - } - ], - "timestampMessage": "Updated from vehicle 3/11/2021 08:02 AM" - }, - "telematicsUnit": "TCB1", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 158, - "green": 158, - "red": 158 - } - }, - "vin": "some_vin_F11", - "year": 2012 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F31/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F31/vehicles_v2_bmw_0.json deleted file mode 100644 index 7583a8b03a2f0..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F31/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,281 +0,0 @@ -[ - { - "a4aType": "USB_ONLY", - "bodyType": "F31", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Ventilation" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "climateTimer": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true, - "page": { - "description": "By setting a start time you let the vehicle know when you plan to use it.", - "primaryButtonText": "SEND TO VEHICLE", - "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE", - "subtitle": "Set start time", - "title": "Ventilation timer" - }, - "tile": { - "description": "Plan start time", - "iconId": 59774, - "title": "Ventilation timer" - } - }, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": false, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": false, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": false, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": false, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": false, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "NOT_CAPABLE" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [], - "driveTrain": "COMBUSTION", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "F020-13-11-502", - "exFactoryPUStep": "1113", - "headUnit": "ID5", - "hmiVersion": "ID4", - "iStep": "F020-13-11-502", - "isLscSupported": false, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "320d xDrive", - "properties": { - "checkControlMessages": [], - "climateControl": {}, - "doorsAndWindows": { - "doors": {}, - "windows": {} - }, - "fuelLevel": { - "units": "LITERS", - "value": 32 - }, - "inMotion": false, - "isServiceRequired": true, - "lastUpdatedAt": "2021-11-01T16:02:44Z", - "originCountryISO": "DE", - "serviceRequired": [ - { - "dateTime": "2021-11-01T00:00:00.000Z", - "status": "PENDING", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2022-07-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 9000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2022-07-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 9000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "dateTime": "2023-02-01T00:00:00.000Z", - "status": "OK", - "type": "VEHICLE_TUV" - } - ] - }, - "puStep": "1113", - "status": { - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59726, - "state": "Unknown", - "title": "All doors" - }, - { - "criticalness": "nonCritical", - "iconId": 59701, - "state": "Unknown", - "title": "Left front window" - }, - { - "criticalness": "nonCritical", - "iconId": 59700, - "state": "Unknown", - "title": "Right front window" - }, - { - "criticalness": "nonCritical", - "iconId": 59703, - "state": "Unknown", - "title": "Left rear window" - }, - { - "criticalness": "nonCritical", - "iconId": 59702, - "state": "Unknown", - "title": "Right rear window" - }, - { - "criticalness": "nonCritical", - "iconId": 59721, - "state": "Unknown", - "title": "Back window" - } - ], - "doorsGeneralState": "Unknown", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": true, - "levelIconId": 59682, - "levelUnits": "l", - "levelValue": "32", - "mainBarValue": 0, - "rangeIconId": 59681, - "rangeUnits": "km", - "rangeValue": "- -", - "secondaryBarValue": 0, - "showsBar": false - } - ], - "lastUpdatedAt": "2021-11-01T16:02:44Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "semiCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Service due soon. Please make an appointment with your service center.", - "subtitle": "Due in November 2021", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in July 2022 or 9000 km", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in July 2022 or 9000 km", - "title": "Vehicle check" - }, - { - "criticalness": "nonCritical", - "iconId": 60111, - "id": "VehicleAdmissionTest", - "longDescription": "Next state inspection due by the specified date.", - "subtitle": "Due in February 2023", - "title": "Vehicle Inspection" - } - ], - "timestampMessage": "Updated from vehicle 11/1/2021 05:02 PM" - }, - "telematicsUnit": "TCB1", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 51, - "green": 51, - "red": 51 - } - }, - "vin": "some_vin_F31", - "year": 2013 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F44/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F44/vehicles_v2_bmw_0.json deleted file mode 100644 index 12ea886d7f5a6..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F44/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,251 +0,0 @@ -[ - { - "a4aType": "NOT_SUPPORTED", - "bodyType": "F44", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": false, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": false, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": false, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": false, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": false, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "remoteSoftwareUpgrade": { - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [], - "driveTrain": "COMBUSTION", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "S18A-20-11-538", - "exFactoryPUStep": "1120", - "headUnit": "MGU", - "hmiVersion": "id7", - "iStep": "S18A-21-03-550", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "218i", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "checkControlMessages": [], - "climateControl": { - "activity": "INACTIVE" - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 222 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 16 - }, - "fuelPercentage": { - "value": 35 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-10T18:03:43Z", - "originCountryISO": "BE", - "serviceRequired": [ - { - "dateTime": "2022-12-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 24000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2023-12-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2024-12-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 50000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - } - ], - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "0321", - "status": { - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "3047", - "mileage": 3047, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors and windows" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": 59682, - "levelUnits": "%", - "levelValue": "35", - "mainBarValue": 35, - "rangeIconId": 59681, - "rangeUnits": "km", - "rangeValue": "222", - "secondaryBarValue": 0, - "showsBar": true - } - ], - "issues": {}, - "lastUpdatedAt": "2021-11-10T18:03:43Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in December 2022 or 24000 km", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in December 2023", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in December 2024 or 50000 km", - "title": "Vehicle check" - } - ], - "timestampMessage": "Updated from vehicle 11/10/2021 07:03 PM" - }, - "telematicsUnit": "ATM02", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 84, - "green": 84, - "red": 84 - } - }, - "vin": "some_vin_F44", - "year": 2020 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F45/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F45/vehicles_v2_bmw_0.json deleted file mode 100644 index 0b7102ed083aa..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F45/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,301 +0,0 @@ -[ - { - "a4aType": "USB_ONLY", - "bodyType": "F45", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [], - "driveTrain": "PLUGIN_HYBRID", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "F056-16-07-502", - "exFactoryPUStep": "0716", - "headUnit": "ID5", - "hmiVersion": "ID4", - "iStep": "F056-20-07-550", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "225xe iPerformance", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 40, - "isChargerConnected": false, - "state": "NOT_CHARGING", - "type": "CONDUCTIVE" - }, - "checkControlMessages": [], - "climateControl": {}, - "combinedRange": { - "distance": { - "units": "KILOMETERS", - "value": 245 - } - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 245 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 4 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 40, - "distance": { - "units": "KILOMETERS", - "value": 4 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 20 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-10T18:25:38Z", - "originCountryISO": "GB", - "serviceRequired": [], - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "0720", - "status": { - "chargingProfile": { - "chargingControlType": "twoWeeksTimer", - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": false, - "departureTimes": [ - { - "action": "deactivate", - "id": 1, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 2, - "timerWeekDays": [] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 16, - "minute": 0 - }, - "start": { - "hour": 13, - "minute": 0 - } - } - }, - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "66720", - "mileage": 66720, - "units": "mi" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors and windows" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59691, - "infoLabel": "Combined Range", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": null, - "levelUnits": null, - "levelValue": null, - "mainBarValue": 0, - "rangeIconId": 59691, - "rangeUnits": "mi", - "rangeValue": "152", - "secondaryBarValue": 0, - "showsBar": false - }, - { - "barType": null, - "chargingStatusIndicatorType": "DEFAULT", - "chargingStatusType": "DEFAULT", - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59694, - "infoLabel": "State of Charge", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": 59694, - "levelUnits": "%", - "levelValue": "40", - "mainBarValue": 40, - "rangeIconId": 59683, - "rangeUnits": "mi", - "rangeValue": "2", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - }, - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": true, - "levelIconId": 59682, - "levelUnits": "l", - "levelValue": "20", - "mainBarValue": 0, - "rangeIconId": 59681, - "rangeUnits": "mi", - "rangeValue": "150", - "secondaryBarValue": 0, - "showsBar": false - } - ], - "issues": {}, - "lastUpdatedAt": "2021-11-10T18:25:38Z", - "recallExternalUrl": null, - "recallMessages": [], - "timestampMessage": "Updated from vehicle 11/11/2021 06:25 PM" - }, - "telematicsUnit": "TCB1", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 66, - "green": 66, - "red": 66 - } - }, - "vin": "some_vin_F45", - "year": 2016 - } - ] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F48/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F48/vehicles_v2_bmw_0.json deleted file mode 100644 index 30d61fb94a4a9..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/F48/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,278 +0,0 @@ -[ - { - "a4aType": "BLUETOOTH", - "bodyType": "F48", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Ventilation" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "climateTimer": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true, - "page": { - "description": "By setting a start time you let the vehicle know when you plan to use it.", - "primaryButtonText": "SEND TO VEHICLE", - "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE", - "subtitle": "Set start time", - "title": "Ventilation timer" - }, - "tile": { - "description": "Plan start time", - "iconId": 59774, - "title": "Ventilation timer" - } - }, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": false, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": false, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": false, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": false, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": false, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [ - "WIFI_HOTSPOT_SERVICE" - ], - "driveTrain": "COMBUSTION", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "F056-17-07-503", - "exFactoryPUStep": "0717", - "headUnit": "ID5", - "hmiVersion": "ID5", - "iStep": "F056-18-03-541", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "X1 sDrive18i", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "checkControlMessages": [], - "climateControl": {}, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 308 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 19 - }, - "inMotion": true, - "isServiceRequired": false, - "lastUpdatedAt": "2021-10-30T06:57:45Z", - "originCountryISO": "NL", - "serviceRequired": [ - { - "dateTime": "2021-12-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 8000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2021-12-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 8000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "dateTime": "2022-07-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - } - ], - "vehicleLocation": { - "address": { - "formatted": "address, city" - }, - "coordinates": { - "latitude": 0.0, - "longitude": 0.0 - }, - "heading": 123 - } - }, - "puStep": "0318", - "status": { - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "113009", - "mileage": 113009, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors and windows" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": true, - "levelIconId": 59682, - "levelUnits": "l", - "levelValue": "19", - "mainBarValue": 0, - "rangeIconId": 59681, - "rangeUnits": "km", - "rangeValue": "308", - "secondaryBarValue": 0, - "showsBar": false - } - ], - "issues": {}, - "lastUpdatedAt": "2021-10-30T06:57:45Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in December 2021 or 8000 km", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in December 2021 or 8000 km", - "title": "Vehicle check" - }, - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in July 2022", - "title": "Brake fluid" - } - ], - "timestampMessage": "Updated from vehicle 10/30/2021 08:57 AM" - }, - "telematicsUnit": "ATM", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 158, - "green": 158, - "red": 158 - } - }, - "vin": "some_vin_F48", - "year": 2017 - } -] diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G01/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G01/vehicles_v2_bmw_0.json deleted file mode 100644 index e12a0ef3a6e14..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G01/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,429 +0,0 @@ -[ - { - "a4aType": "USB_ONLY", - "bodyType": "G01", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [], - "driveTrain": "PLUGIN_HYBRID", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "S15A-20-07-549", - "exFactoryPUStep": "0720", - "headUnit": "ID5", - "hmiVersion": "ID5", - "iStep": "S15A-20-07-549", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "X3 xDrive30e", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 12, - "isChargerConnected": true, - "state": "CHARGING", - "type": "NOT_AVAILABLE" - }, - "checkControlMessages": [], - "climateControl": {}, - "combinedRange": { - "distance": { - "units": "KILOMETERS", - "value": 439 - } - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 439 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 2 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 12, - "distance": { - "units": "KILOMETERS", - "value": 2 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 30 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-12-13T21:06:27Z", - "originCountryISO": "ES", - "serviceRequired": [ - { - "dateTime": "2024-12-01T00:00:00.000Z", - "status": "OK", - "type": "VEHICLE_TUV" - }, - { - "dateTime": "2022-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 12000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2024-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 45000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "dateTime": "2023-10-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - } - ], - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "0720", - "status": { - "chargingProfile": { - "chargingControlType": "weeklyPlanner", - "chargingMode": "delayedCharging", - "chargingPreference": "chargingWindow", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": false, - "departureTimes": [ - { - "action": "activate", - "id": 1, - "timeStamp": { - "hour": 17, - "minute": 0 - }, - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ] - }, - { - "action": "deactivate", - "id": 2, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 3, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 4, - "timeStamp": { - "hour": 17, - "minute": 0 - }, - "timerWeekDays": [ - "tuesday" - ] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 16, - "minute": 59 - }, - "start": { - "hour": 9, - "minute": 0 - } - } - }, - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "21068", - "mileage": 21068, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59757, - "state": "Locked", - "title": "Lock status" - }, - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors" - }, - { - "criticalness": "nonCritical", - "iconId": 59725, - "state": "Closed", - "title": "All windows" - }, - { - "criticalness": "nonCritical", - "iconId": 59706, - "state": "Closed", - "title": "Hood" - }, - { - "criticalness": "nonCritical", - "iconId": 59704, - "state": "Closed", - "title": "Trunk" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59691, - "infoLabel": "Combined Range", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": null, - "levelUnits": null, - "levelValue": null, - "mainBarValue": 0, - "rangeIconId": 59691, - "rangeUnits": "km", - "rangeValue": "439", - "secondaryBarValue": 0, - "showsBar": false - }, - { - "barType": null, - "chargingStatusIndicatorType": "PLUGGED_IN", - "chargingStatusType": "PLUGGED_IN", - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59689, - "infoLabel": "Starts at ~ 09:00 AM", - "isCircleIcon": true, - "isInaccurate": true, - "levelIconId": 59689, - "levelUnits": "%", - "levelValue": "12", - "mainBarValue": 12, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "2", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - }, - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": true, - "levelIconId": 59682, - "levelUnits": "l", - "levelValue": "30", - "mainBarValue": 0, - "rangeIconId": 59681, - "rangeUnits": "km", - "rangeValue": "437", - "secondaryBarValue": 0, - "showsBar": false - } - ], - "issues": {}, - "lastUpdatedAt": "2021-12-13T21:06:27Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60111, - "id": "VehicleAdmissionTest", - "longDescription": "Next state inspection due by the specified date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Vehicle Inspection" - }, - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Vehicle check" - }, - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Brake fluid" - } - ], - "timestampMessage": "Updated from vehicle 12/13/2021 10:06 PM" - }, - "telematicsUnit": "ATM", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 133, - "green": 129, - "red": 127 - } - }, - "vin": "some_vin_G01", - "year": 2020 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G05/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G05/vehicles_v2_bmw_0.json deleted file mode 100644 index bdcf55a6e9a18..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G05/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,401 +0,0 @@ -[ - { - "a4aType": "NOT_SUPPORTED", - "bodyType": "G05", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": true, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "remote360": { - "isComingSoonEnabled": false, - "isDataPrivacyEnabled": false, - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true - }, - "remoteSoftwareUpgrade": { - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [ - "WIFI_HOTSPOT_SERVICE" - ], - "driveTrain": "PLUGIN_HYBRID", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "S18A-21-03-563", - "exFactoryPUStep": "0321", - "headUnit": "MGU", - "hmiVersion": "id7", - "iStep": "S18A-21-07-550", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "X5 xDrive45e", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 80, - "isChargerConnected": true, - "state": "CHARGING", - "type": "NOT_AVAILABLE" - }, - "checkControlMessages": [], - "climateControl": { - "activity": "INACTIVE" - }, - "combinedRange": { - "distance": { - "units": "KILOMETERS", - "value": 466 - } - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 466 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "moonroof": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 48 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 80, - "distance": { - "units": "KILOMETERS", - "value": 48 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 47 - }, - "fuelPercentage": { - "value": 74 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-10T22:21:39Z", - "originCountryISO": "BE", - "serviceRequired": [ - { - "dateTime": "2023-06-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 32000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2024-06-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2025-06-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 60000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - } - ], - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "0721", - "status": { - "chargingProfile": { - "chargingControlType": "weeklyPlanner", - "chargingMode": "immediateCharging", - "chargingPreference": "noPreSelection", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": true, - "departureTimes": [ - { - "action": "deactivate", - "id": 1, - "timerWeekDays": [] - }, - { - "action": "activate", - "id": 2, - "timeStamp": { - "hour": 8, - "minute": 10 - }, - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday" - ] - }, - { - "action": "deactivate", - "id": 3, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 4, - "timeStamp": { - "hour": 8, - "minute": 10 - }, - "timerWeekDays": [ - "thursday" - ] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 0, - "minute": 0 - }, - "start": { - "hour": 0, - "minute": 0 - } - } - }, - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "2667", - "mileage": 2667, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors and windows" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59691, - "infoLabel": "Combined Range", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": null, - "levelUnits": null, - "levelValue": null, - "mainBarValue": 0, - "rangeIconId": 59691, - "rangeUnits": "km", - "rangeValue": "466", - "secondaryBarValue": 0, - "showsBar": false - }, - { - "barType": null, - "chargingStatusIndicatorType": "CHARGING", - "chargingStatusType": "CHARGING", - "chargingType": "charging", - "iconOpacity": "high", - "infoIconId": 59689, - "infoLabel": "100% at ~03:53 AM", - "isCircleIcon": true, - "isInaccurate": true, - "levelIconId": 59689, - "levelUnits": "%", - "levelValue": "80", - "mainBarValue": 80, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "48", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - }, - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": 59682, - "levelUnits": "%", - "levelValue": "74", - "mainBarValue": 74, - "rangeIconId": 59681, - "rangeUnits": "km", - "rangeValue": "418", - "secondaryBarValue": 0, - "showsBar": true - } - ], - "issues": {}, - "lastUpdatedAt": "2021-11-10T22:21:39Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in June 2023 or 32000 km", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in June 2024", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in June 2025 or 60000 km", - "title": "Vehicle check" - } - ], - "timestampMessage": "Updated from vehicle 11/10/2021 11:21 PM" - }, - "telematicsUnit": "ATM02", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 77, - "green": 38, - "red": 31 - } - }, - "vin": "some_vin_G05", - "year": 2021 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G08/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G08/vehicles_v2_bmw_0.json deleted file mode 100644 index e0f89db04a5bf..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G08/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,401 +0,0 @@ -[ - { - "a4aType": "NOT_SUPPORTED", - "bodyType": "G08", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": true, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "remote360": { - "isComingSoonEnabled": false, - "isDataPrivacyEnabled": true, - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true - }, - "remoteSoftwareUpgrade": { - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [ - "WIFI_HOTSPOT_SERVICE" - ], - "driveTrain": "ELECTRIC", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "S15C-20-11-542", - "exFactoryPUStep": "1120", - "headUnit": "MGU", - "hmiVersion": "id7", - "iStep": "S15C-20-11-542", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "iX3", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 50, - "isChargerConnected": true, - "state": "CHARGING", - "type": "NOT_AVAILABLE" - }, - "checkControlMessages": [], - "climateControl": { - "activity": "INACTIVE" - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 186 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "moonroof": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 179 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 50, - "distance": { - "units": "KILOMETERS", - "value": 179 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 0 - }, - "fuelPercentage": { - "value": 0 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-14T15:08:24Z", - "originCountryISO": "NL", - "serviceRequired": [ - { - "dateTime": "2023-06-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2023-06-01T00:00:00.000Z", - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "dateTime": "2025-06-01T00:00:00.000Z", - "status": "OK", - "type": "VEHICLE_TUV" - } - ], - "tires": { - "frontLeft": { - "status": { - "currentPressure": 220, - "localizedCurrentPressure": "2.2 bar", - "localizedTargetPressure": "2.3 bar", - "targetPressure": 231 - } - }, - "frontRight": { - "status": { - "currentPressure": 222, - "localizedCurrentPressure": "2.2 bar", - "localizedTargetPressure": "2.3 bar", - "targetPressure": 233 - } - }, - "rearLeft": { - "status": { - "currentPressure": 264, - "localizedCurrentPressure": "2.6 bar", - "localizedTargetPressure": "2.6 bar", - "targetPressure": 265 - } - }, - "rearRight": { - "status": { - "currentPressure": 266, - "localizedCurrentPressure": "2.6 bar", - "localizedTargetPressure": "2.6 bar", - "targetPressure": 267 - } - } - }, - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "1120", - "status": { - "chargingProfile": { - "chargingControlType": "weeklyPlanner", - "chargingMode": "immediateCharging", - "chargingPreference": "noPreSelection", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "AUTOMATIC_INTELLIGENT", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": true, - "departureTimes": [ - { - "action": "deactivate", - "id": 1, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 2, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 3, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 4, - "timerWeekDays": [] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 0, - "minute": 0 - }, - "start": { - "hour": 0, - "minute": 0 - } - } - }, - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60117, - "state": "OK", - "title": "Tires" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "9527", - "mileage": 9527, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59757, - "state": "Locked", - "title": "Lock status" - }, - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors" - }, - { - "criticalness": "nonCritical", - "iconId": 59725, - "state": "Closed", - "title": "All windows" - }, - { - "criticalness": "nonCritical", - "iconId": 59706, - "state": "Closed", - "title": "Hood" - }, - { - "criticalness": "nonCritical", - "iconId": 59704, - "state": "Closed", - "title": "Trunk" - }, - { - "criticalness": "nonCritical", - "iconId": 59705, - "state": "Closed", - "title": "Sunroof" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "barType": null, - "chargingStatusIndicatorType": "CHARGING", - "chargingStatusType": "CHARGING", - "chargingType": "charging", - "iconOpacity": "high", - "infoIconId": 59689, - "infoLabel": "100% at ~04:01 AM", - "isCircleIcon": true, - "isInaccurate": true, - "levelIconId": 59689, - "levelUnits": "%", - "levelValue": "50", - "mainBarValue": 50, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "179", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - } - ], - "issues": {}, - "lastUpdatedAt": "2021-11-14T15:08:24Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Vehicle check" - }, - { - "criticalness": "nonCritical", - "iconId": 60111, - "id": "VehicleAdmissionTest", - "longDescription": "Next state inspection due by the specified date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Vehicle Inspection" - } - ], - "timestampMessage": "Updated from vehicle 11/14/2021 04:08 PM" - }, - "telematicsUnit": "ATM02", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 173, - "green": 173, - "red": 173 - } - }, - "vin": "some_vin_G08", - "year": 2021 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/340i.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/340i.json deleted file mode 100644 index d7d11adefc8cc..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/340i.json +++ /dev/null @@ -1,401 +0,0 @@ -[ - { - "vin": "anonymous", - "model": "M340i xDrive", - "year": 2021, - "brand": "BMW", - "headUnit": "MGU", - "isLscSupported": true, - "driveTrain": "HYBRID", - "puStep": "0721", - "iStep": "S18A-21-07-550", - "telematicsUnit": "ATM02", - "hmiVersion": "id7", - "bodyType": "G21", - "a4aType": "NOT_SUPPORTED", - "capabilities": { - "isRemoteServicesBookingRequired": false, - "isRemoteServicesActivationRequired": false, - "lock": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "unlock": { - "isEnabled": true, - "isPinAuthenticationRequired": true, - "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "lights": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern." - }, - "horn": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern." - }, - "vehicleFinder": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern." - }, - "speechThirdPartyAlexa": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Alexa aktivieren? Remote-Funktionen können einige Sekunden dauern." - }, - "sendPoi": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern." - }, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "climateNow": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern.", - "executionPopup": { - "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern.", - "popupType": "DIALOG", - "title": "Belüftung starten", - "primaryButtonText": "Start", - "secondaryButtonText": "Abbrechen", - "iconId": 59733 - }, - "executionStopPopup": { - "executionMessage": "Jetzt Klimatisierung Ihres Fahrzeugs beenden? Remote-Funktionen können einige Sekunden dauern.", - "title": "Klimatisierung läuft" - } - }, - "isRemoteHistorySupported": true, - "canRemoteHistoryBeDeleted": false, - "remoteSoftwareUpgrade": { - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "climateTimer": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "tile": { - "iconId": 59774, - "title": "Belüftungstimer", - "description": "Abfahrtszeit einstellen" - }, - "page": { - "primaryButtonText": "AN FAHRZEUG SENDEN", - "secondaryButtonText": "DEAKTIVIEREN UND AN FAHRZEUG SENDEN", - "title": "Belüftungstimer", - "subtitle": "Abfahrtszeit einstellen", - "description": "Durch das Einstellen einer Abfahrtszeit teilen Sie dem Fahrzeug mit, wann Sie es benutzen wollen." - }, - "isToggleEnabled": true - }, - "isChargingHistorySupported": false, - "isScanAndChargeSupported": false, - "isDCSContractManagementSupported": false, - "isBmwChargingSupported": false, - "isMiniChargingSupported": false, - "isChargeNowForBusinessSupported": false, - "isDataPrivacyEnabled": false, - "isChargingPlanSupported": false, - "isChargingPowerLimitEnable": false, - "isChargingTargetSocEnable": false, - "isChargingLoudnessEnable": false, - "isChargingSettingsEnabled": false, - "isChargingHospitalityEnabled": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": false, - "isCustomerEsimSupported": false, - "isCarSharingSupported": false, - "isEasyChargeSupported": false, - "isSustainabilitySupported": false - }, - "connectedDriveServices": [], - "properties": { - "lastUpdatedAt": "2022-03-01T07:00:27Z", - "inMotion": false, - "areDoorsLocked": true, - "originCountryISO": "DE", - "areDoorsClosed": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "trunk": "CLOSED", - "hood": "CLOSED", - "moonroof": "CLOSED" - }, - "tires": { - "frontLeft": { - "status": { - "currentPressure": 280, - "localizedCurrentPressure": "2,8 bar", - "targetPressure": 290, - "localizedTargetPressure": "2,9 bar", - "wear": 0 - } - }, - "frontRight": { - "status": { - "currentPressure": 280, - "localizedCurrentPressure": "2,8 bar", - "targetPressure": 290, - "localizedTargetPressure": "2,9 bar", - "wear": 0 - } - }, - "rearLeft": { - "status": { - "currentPressure": 280, - "localizedCurrentPressure": "2,8 bar", - "targetPressure": 290, - "localizedTargetPressure": "2,9 bar", - "wear": 0 - } - }, - "rearRight": { - "status": { - "currentPressure": 280, - "localizedCurrentPressure": "2,8 bar", - "targetPressure": 290, - "localizedTargetPressure": "2,9 bar", - "wear": 0 - } - } - }, - "isServiceRequired": false, - "fuelLevel": { - "value": 36, - "units": "LITERS" - }, - "fuelPercentage": { - "value": 69 - }, - "combustionRange": { - "distance": { - "value": 404, - "units": "KILOMETERS" - } - }, - "checkControlMessages": [], - "serviceRequired": [ - { - "type": "OIL", - "status": "OK", - "dateTime": "2023-06-01T00:00:00.000Z", - "distance": { - "value": 29000, - "units": "KILOMETERS" - } - }, - { - "type": "BRAKE_FLUID", - "status": "OK", - "dateTime": "2024-06-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_TUV", - "status": "OK", - "dateTime": "2024-08-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_CHECK", - "status": "OK", - "dateTime": "2025-06-01T00:00:00.000Z", - "distance": { - "value": 60000, - "units": "KILOMETERS" - } - }, - { - "type": "TIRE_WEAR_FRONT", - "status": "OK" - }, - { - "type": "TIRE_WEAR_REAR", - "status": "OK" - } - ], - "vehicleLocation": { - "coordinates": { - "latitude": 1.1, - "longitude": 2.2 - }, - "address": { - "formatted": "anonymous" - }, - "heading": -1 - }, - "climateControl": { - "activity": "INACTIVE" - } - }, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "driverGuideInfo": { - "title": "BMW\nDriver's Guide", - "androidAppScheme": "com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" - }, - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "red": 40, - "green": 94, - "blue": 201 - } - }, - "status": { - "lastUpdatedAt": "2022-03-01T07:00:27Z", - "currentMileage": { - "mileage": 4955, - "units": "km", - "formattedMileage": "4.955" - }, - "issues": null, - "doorsGeneralState": "Verriegelt", - "checkControlMessagesGeneralState": "Keine Probleme", - "doorsAndWindows": [ - { - "iconId": 59757, - "title": "Verriegelungsstatus", - "state": "Verriegelt", - "criticalness": "nonCritical" - }, - { - "iconId": 59722, - "title": "Alle Türen", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59725, - "title": "Alle Fenster", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59706, - "title": "Frontklappe", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59704, - "title": "Gepäckraum", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59705, - "title": "Glasdach", - "state": "Geschlossen", - "criticalness": "nonCritical" - } - ], - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60117, - "title": "Reifen", - "state": "OK" - }, - { - "criticalness": "nonCritical", - "iconId": 60197, - "title": "Motoröl", - "state": "OK" - } - ], - "requiredServices": [ - { - "id": "Oil", - "title": "Motoröl", - "iconId": 60197, - "longDescription": "Nächster Service nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im Juni 2023 oder in 29.000 km", - "criticalness": "nonCritical" - }, - { - "id": "BrakeFluid", - "title": "Bremsflüssigkeit", - "iconId": 60223, - "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.", - "subtitle": "Fällig im Juni 2024", - "criticalness": "nonCritical" - }, - { - "id": "VehicleAdmissionTest", - "title": "Fahrzeuginspektion (HU)", - "iconId": 60111, - "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.", - "subtitle": "Fällig im August 2024", - "criticalness": "nonCritical" - }, - { - "id": "VehicleCheck", - "title": "Fahrzeug-Check", - "iconId": 60215, - "longDescription": "Nächste Sichtprüfung zum angegebenen Termin oder nach der ggf. angegebenen Fahrstrecke.", - "subtitle": "Fällig im Juni 2025 oder in 60.000 km", - "criticalness": "nonCritical" - }, - { - "id": "TireWearFront", - "title": "Reifenservice Vorderreifen", - "iconId": 60447, - "subtitle": "OK", - "criticalness": "nonCritical" - }, - { - "id": "TireWearRear", - "title": "Reifenservice Hinterreifen", - "iconId": 60447, - "subtitle": "OK", - "criticalness": "nonCritical" - } - ], - "recallMessages": [], - "recallExternalUrl": null, - "fuelIndicators": [ - { - "secondaryBarValue": 0, - "infoIconId": 59930, - "infoLabel": "Tankfüllstand", - "rangeIconId": 59681, - "rangeUnits": "km", - "rangeValue": "404", - "levelIconId": 59682, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null, - "mainBarValue": 69, - "showsBar": true, - "levelUnits": "%", - "levelValue": "69", - "isInaccurate": false - } - ], - "timestampMessage": "Aktualisiert vom Fahrzeug 1.3.2022 08:00 AM" - }, - "exFactoryPUStep": "0321", - "exFactoryILevel": "S18A-21-03-555" - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-sessions_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-sessions_0.json deleted file mode 100644 index 8c5769a61e373..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-sessions_0.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "chargingSessions": { - "chargingListState": "HAS_SESSIONS", - "numberOfSessions": "11", - "sessions": [ - { - "energyCharged": "~ 6 kWh", - "id": "2021-11-14T16:42:29Z_39df2d52", - "isPublic": true, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Today 5:42 PM" - }, - { - "energyCharged": "~ 11 kWh", - "id": "2021-11-13T18:15:24Z_39df2d52", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Yesterday 7:15 PM" - }, - { - "energyCharged": "~ 10 kWh", - "id": "2021-11-11T18:01:58Z_39df2d52", - "isPublic": true, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Thursday 7:01 PM" - }, - { - "energyCharged": "~ 12 kWh", - "id": "2021-11-09T20:33:19Z_39df2d52", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Tuesday 9:33 PM" - }, - { - "energyCharged": "< 2 kWh", - "id": "2021-11-09T12:34:28Z_39df2d52", - "isPublic": true, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Tuesday 1:34 PM" - }, - { - "energyCharged": "~ 7 kWh", - "id": "2021-11-08T18:31:20Z_39df2d52", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Monday 7:31 PM" - }, - { - "energyCharged": "~ 10 kWh", - "id": "2021-11-06T16:52:16Z_39df2d52", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "11/6/2021 5:52 PM" - }, - { - "energyCharged": "~ 10 kWh", - "id": "2021-11-04T18:24:22Z_39df2d52", - "isPublic": true, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "11/4/2021 7:24 PM" - }, - { - "energyCharged": "~ 6 kWh", - "id": "2021-11-03T14:24:06Z_39df2d52", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "11/3/2021 3:24 PM" - }, - { - "energyCharged": "~ 13 kWh", - "id": "2021-11-02T19:20:57Z_39df2d52", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "11/2/2021 8:20 PM" - }, - { - "energyCharged": "< 2 kWh", - "id": "2021-11-01T15:04:09Z_39df2d52", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "11/1/2021 4:04 PM" - } - ], - "total": "~ 87 kWh" - }, - "datePicker": { - "endDate": "2021-11-14T20:20:25Z", - "selectedDate": "2021-11-14T16:42:29Z", - "startDate": "2020-10-21T14:21:31Z" - }, - "paginationInfo": {} -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-statistics_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-statistics_0.json deleted file mode 100644 index a595ee9db4a8c..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/charging-statistics_0.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "description": "November 2021", - "optStateType": "OPT_IN_WITH_SESSIONS", - "statistics": { - "numberOfChargingSessions": 11, - "numberOfChargingSessionsSemantics": "mobile20chsChargingSessionNumberSemantics", - "symbol": "~", - "totalEnergyCharged": 87, - "totalEnergyChargedSemantics": "mobile20chsApproximatelyTotalChargedSemantics" - } -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/json_export.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/json_export.json deleted file mode 100644 index 7c98cb1348d85..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/json_export.json +++ /dev/null @@ -1,831 +0,0 @@ -{ - "attributes": { - "a4aType": "NOT_SUPPORTED", - "bodyType": "G21", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": true, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": true, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "remote360": { - "isComingSoonEnabled": false, - "isDataPrivacyEnabled": false, - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true - }, - "remoteSoftwareUpgrade": { - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "speechThirdPartyAlexa": { - "executionMessage": "Activate Alexa now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [ - "WIFI_HOTSPOT_SERVICE" - ], - "driveTrain": "PLUGIN_HYBRID", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "S18A-20-07-548", - "exFactoryPUStep": "0720", - "headUnit": "MGU", - "hmiVersion": "id7", - "iStep": "S18A-21-07-550", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "330e xDrive", - "puStep": "0721", - "telematicsUnit": "ATM02", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 201, - "green": 94, - "red": 40 - } - }, - "vin": "some_vin_G21", - "year": 2020 - }, - "status": { - "status": { - "chargingProfile": { - "chargingControlType": "weeklyPlanner", - "chargingMode": "immediateCharging", - "chargingPreference": "noPreSelection", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": false, - "departureTimes": [ - { - "action": "deactivate", - "id": 1, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 2, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 3, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 4, - "timerWeekDays": [] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 0, - "minute": 0 - }, - "start": { - "hour": 0, - "minute": 0 - } - } - }, - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60117, - "state": "OK", - "title": "Tires" - }, - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "27138", - "mileage": 27138, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59757, - "state": "Locked", - "title": "Lock status" - }, - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors" - }, - { - "criticalness": "nonCritical", - "iconId": 59725, - "state": "Closed", - "title": "All windows" - }, - { - "criticalness": "nonCritical", - "iconId": 59706, - "state": "Closed", - "title": "Hood" - }, - { - "criticalness": "nonCritical", - "iconId": 59704, - "state": "Closed", - "title": "Trunk" - }, - { - "criticalness": "nonCritical", - "iconId": 59705, - "state": "Closed", - "title": "Sunroof" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59691, - "infoLabel": "Combined Range", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": null, - "levelUnits": null, - "levelValue": null, - "mainBarValue": 0, - "rangeIconId": 59691, - "rangeUnits": "km", - "rangeValue": "368", - "secondaryBarValue": 0, - "showsBar": false - }, - { - "barType": null, - "chargingStatusIndicatorType": "CHARGING", - "chargingStatusType": "CHARGING", - "chargingType": "charging", - "iconOpacity": "high", - "infoIconId": 59689, - "infoLabel": "100% at ~12:43 AM", - "isCircleIcon": true, - "isInaccurate": true, - "levelIconId": 59689, - "levelUnits": "%", - "levelValue": "83", - "mainBarValue": 83, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "23", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - }, - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": 59682, - "levelUnits": "%", - "levelValue": "83", - "mainBarValue": 83, - "rangeIconId": 59681, - "rangeUnits": "km", - "rangeValue": "345", - "secondaryBarValue": 0, - "showsBar": true - } - ], - "issues": {}, - "lastUpdatedAt": "2021-11-14T20:20:21Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Vehicle check" - }, - { - "criticalness": "nonCritical", - "iconId": 60447, - "id": "TireWearFront", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Tire service, front tires" - }, - { - "criticalness": "nonCritical", - "iconId": 60447, - "id": "TireWearRear", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Tire service, rear tires" - }, - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Brake fluid" - } - ], - "timestampMessage": "Updated from vehicle 11/14/2021 09:20 PM" - }, - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 83, - "isChargerConnected": true, - "state": "CHARGING", - "type": "NOT_AVAILABLE" - }, - "checkControlMessages": [], - "climateControl": { - "activity": "INACTIVE" - }, - "combinedRange": { - "distance": { - "units": "KILOMETERS", - "value": 368 - } - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 368 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "moonroof": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 23 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 83, - "distance": { - "units": "KILOMETERS", - "value": 23 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 29 - }, - "fuelPercentage": { - "value": 83 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-14T20:20:21Z", - "originCountryISO": "DE", - "serviceRequired": [ - { - "dateTime": "2022-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 6000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2024-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 39000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "status": "OK", - "type": "TIRE_WEAR_FRONT" - }, - { - "status": "OK", - "type": "TIRE_WEAR_REAR" - }, - { - "dateTime": "2023-10-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - } - ], - "tires": { - "frontLeft": { - "details": { - "dimension": "225/45 R18 95V XL", - "manufacturer": "Pirelli", - "manufacturingWeek": "35 / 19", - "maxSpeed": "240 km/h", - "mountingDate": "11/12/2021", - "optimizedForOemBmw": "Yes", - "partNumber": "2461777", - "season": "Winter tires", - "treadDesign": "SOTTOZERO 3" - }, - "status": { - "currentPressure": 260, - "localizedCurrentPressure": "2.6 bar", - "localizedTargetPressure": "2.7 bar", - "targetPressure": 270, - "wear": 0 - } - }, - "frontRight": { - "details": { - "dimension": "225/45 R18 95V XL", - "manufacturer": "Pirelli", - "manufacturingWeek": "27 / 19", - "maxSpeed": "240 km/h", - "mountingDate": "11/12/2021", - "optimizedForOemBmw": "Yes", - "partNumber": "2461777", - "season": "Winter tires", - "treadDesign": "SOTTOZERO 3" - }, - "status": { - "currentPressure": 250, - "localizedCurrentPressure": "2.5 bar", - "localizedTargetPressure": "2.7 bar", - "targetPressure": 270, - "wear": 0 - } - }, - "rearLeft": { - "details": { - "dimension": "255/40 R18 99V XL", - "manufacturer": "Pirelli", - "manufacturingWeek": "17 / 19", - "maxSpeed": "240 km/h", - "mountingDate": "11/12/2021", - "optimizedForOemBmw": "Yes", - "partNumber": "2461778", - "season": "Winter tires", - "treadDesign": "SOTTOZERO 3" - }, - "status": { - "currentPressure": 300, - "localizedCurrentPressure": "3.0 bar", - "localizedTargetPressure": "2.9 bar", - "targetPressure": 290, - "wear": 0 - } - }, - "rearRight": { - "details": { - "dimension": "255/40 R18 99V XL", - "manufacturer": "Pirelli", - "manufacturingWeek": "26 / 19", - "maxSpeed": "240 km/h", - "mountingDate": "11/12/2021", - "optimizedForOemBmw": "Yes", - "partNumber": "2461778", - "season": "Winter tires", - "treadDesign": "SOTTOZERO 3" - }, - "status": { - "currentPressure": 250, - "localizedCurrentPressure": "2.5 bar", - "localizedTargetPressure": "2.9 bar", - "targetPressure": 290, - "wear": 0 - } - } - }, - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "all_lids_closed": true, - "all_windows_closed": true, - "are_all_cbs_ok": true, - "are_parking_lights_on": null, - "charging_end_time": "2011-11-29 00:43:00+00:00", - "charging_level_hv": 83, - "charging_start_time": null, - "charging_status": "CHARGING", - "charging_time_label": "100% at ~12:43 AM", - "charging_time_remaining": 3.23, - "check_control_messages": [], - "condition_based_services": [ - { - "due_date": "2022-10-01 00:00:00+00:00", - "state": "OK", - "service_type": "OIL", - "due_distance": [ - 6000, - "KILOMETERS" - ], - "description": null - }, - { - "due_date": "2024-10-01 00:00:00+00:00", - "state": "OK", - "service_type": "VEHICLE_CHECK", - "due_distance": [ - 39000, - "KILOMETERS" - ], - "description": null - }, - { - "due_date": null, - "state": "OK", - "service_type": "TIRE_WEAR_FRONT", - "due_distance": null, - "description": null - }, - { - "due_date": null, - "state": "OK", - "service_type": "TIRE_WEAR_REAR", - "due_distance": null, - "description": null - }, - { - "due_date": "2023-10-01 00:00:00+00:00", - "state": "OK", - "service_type": "BRAKE_FLUID", - "due_distance": null, - "description": null - } - ], - "connection_status": "CONNECTED", - "door_lock_state": "LOCKED", - "fuel_indicator_count": 3, - "fuel_percent": 83, - "gps_heading": 123, - "gps_position": [ - 12.3456, - 34.5678 - ], - "has_check_control_messages": false, - "has_parking_light_state": false, - "is_vehicle_active": false, - "last_charging_end_result": null, - "last_update_reason": "Updated from vehicle 11/14/2021 09:20 PM", - "lids": [ - { - "name": "hood", - "state": "CLOSED" - }, - { - "name": "trunk", - "state": "CLOSED" - }, - { - "name": "driverFront", - "state": "CLOSED" - }, - { - "name": "driverRear", - "state": "CLOSED" - }, - { - "name": "passengerFront", - "state": "CLOSED" - }, - { - "name": "passengerRear", - "state": "CLOSED" - } - ], - "max_range_electric": null, - "mileage": [ - 27138, - "km" - ], - "open_lids": [], - "open_windows": [], - "parking_lights": null, - "remaining_fuel": [ - 29, - "LITERS" - ], - "remaining_range_electric": [ - 23, - "km" - ], - "remaining_range_fuel": [ - 345, - "km" - ], - "remaining_range_total": [ - 368, - "km" - ], - "timestamp": "2021-11-14 20:20:21+00:00", - "windows": [ - { - "name": "driverFront", - "state": "CLOSED" - }, - { - "name": "driverRear", - "state": "CLOSED" - }, - { - "name": "passengerFront", - "state": "CLOSED" - }, - { - "name": "passengerRear", - "state": "CLOSED" - }, - { - "name": "moonroof", - "state": "CLOSED" - } - ] - }, - "observer_latitude": null, - "observer_longitude": null, - "available_attributes": [ - "gps_position", - "vin", - "remaining_range_total", - "mileage", - "charging_time_remaining", - "charging_start_time", - "charging_end_time", - "charging_time_label", - "charging_status", - "charging_level_hv", - "connection_status", - "remaining_range_electric", - "last_charging_end_result", - "remaining_fuel", - "remaining_range_fuel", - "fuel_percent", - "condition_based_services", - "check_control_messages", - "door_lock_state", - "timestamp", - "last_update_reason", - "lids", - "windows" - ], - "available_state_services": [ - "status" - ], - "brand": "bmw", - "charging_profile": { - "charging_profile": { - "chargingControlType": "weeklyPlanner", - "chargingMode": "immediateCharging", - "chargingPreference": "noPreSelection", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": false, - "departureTimes": [ - { - "action": "deactivate", - "id": 1, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 2, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 3, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 4, - "timerWeekDays": [] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 0, - "minute": 0 - }, - "start": { - "hour": 0, - "minute": 0 - } - } - }, - "charging_mode": "immediateCharging", - "charging_preferences": "noPreSelection", - "is_pre_entry_climatization_enabled": false, - "preferred_charging_window": { - "end_time": "00:00", - "start_time": "00:00" - }, - "timer": { - "1": { - "action": "deactivate", - "start_time": "00:00", - "timer_id": 1, - "weekdays": [] - }, - "2": { - "action": "deactivate", - "start_time": "00:00", - "timer_id": 2, - "weekdays": [] - }, - "3": { - "action": "deactivate", - "start_time": "00:00", - "timer_id": 3, - "weekdays": [] - }, - "4": { - "action": "deactivate", - "start_time": null, - "timer_id": 4, - "weekdays": [] - } - } - }, - "drive_train": "PLUGIN_HYBRID", - "drive_train_attributes": [ - "remaining_range_total", - "mileage", - "charging_time_remaining", - "charging_start_time", - "charging_end_time", - "charging_time_label", - "charging_status", - "charging_level_hv", - "connection_status", - "remaining_range_electric", - "last_charging_end_result", - "remaining_fuel", - "remaining_range_fuel", - "fuel_percent" - ], - "has_hv_battery": true, - "has_internal_combustion_engine": true, - "has_range_extender": false, - "has_weekly_planner_service": true, - "is_vehicle_tracking_enabled": true, - "lsc_type": "ACTIVATED", - "name": "330e xDrive" -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/vehicles_v2_bmw_0.json deleted file mode 100644 index 0d4a372eb6eb0..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G21/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,542 +0,0 @@ -[ - { - "a4aType": "NOT_SUPPORTED", - "bodyType": "G21", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": true, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": true, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "remote360": { - "isComingSoonEnabled": false, - "isDataPrivacyEnabled": false, - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true - }, - "remoteSoftwareUpgrade": { - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "speechThirdPartyAlexa": { - "executionMessage": "Activate Alexa now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [ - "WIFI_HOTSPOT_SERVICE" - ], - "driveTrain": "PLUGIN_HYBRID", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "S18A-20-07-548", - "exFactoryPUStep": "0720", - "headUnit": "MGU", - "hmiVersion": "id7", - "iStep": "S18A-21-07-550", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "330e xDrive", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 83, - "isChargerConnected": true, - "state": "CHARGING", - "type": "NOT_AVAILABLE" - }, - "checkControlMessages": [], - "climateControl": { - "activity": "INACTIVE" - }, - "combinedRange": { - "distance": { - "units": "KILOMETERS", - "value": 368 - } - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 368 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "moonroof": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 23 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 83, - "distance": { - "units": "KILOMETERS", - "value": 23 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 29 - }, - "fuelPercentage": { - "value": 83 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-14T20:20:21Z", - "originCountryISO": "DE", - "serviceRequired": [ - { - "dateTime": "2022-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 6000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2024-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 39000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "status": "OK", - "type": "TIRE_WEAR_FRONT" - }, - { - "status": "OK", - "type": "TIRE_WEAR_REAR" - }, - { - "dateTime": "2023-10-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - } - ], - "tires": { - "frontLeft": { - "details": { - "dimension": "225/45 R18 95V XL", - "manufacturer": "Pirelli", - "manufacturingWeek": "35 / 19", - "maxSpeed": "240 km/h", - "mountingDate": "11/12/2021", - "optimizedForOemBmw": "Yes", - "partNumber": "2461777", - "season": "Winter tires", - "treadDesign": "SOTTOZERO 3" - }, - "status": { - "currentPressure": 260, - "localizedCurrentPressure": "2.6 bar", - "localizedTargetPressure": "2.7 bar", - "targetPressure": 270, - "wear": 0 - } - }, - "frontRight": { - "details": { - "dimension": "225/45 R18 95V XL", - "manufacturer": "Pirelli", - "manufacturingWeek": "27 / 19", - "maxSpeed": "240 km/h", - "mountingDate": "11/12/2021", - "optimizedForOemBmw": "Yes", - "partNumber": "2461777", - "season": "Winter tires", - "treadDesign": "SOTTOZERO 3" - }, - "status": { - "currentPressure": 250, - "localizedCurrentPressure": "2.5 bar", - "localizedTargetPressure": "2.7 bar", - "targetPressure": 270, - "wear": 0 - } - }, - "rearLeft": { - "details": { - "dimension": "255/40 R18 99V XL", - "manufacturer": "Pirelli", - "manufacturingWeek": "17 / 19", - "maxSpeed": "240 km/h", - "mountingDate": "11/12/2021", - "optimizedForOemBmw": "Yes", - "partNumber": "2461778", - "season": "Winter tires", - "treadDesign": "SOTTOZERO 3" - }, - "status": { - "currentPressure": 300, - "localizedCurrentPressure": "3.0 bar", - "localizedTargetPressure": "2.9 bar", - "targetPressure": 290, - "wear": 0 - } - }, - "rearRight": { - "details": { - "dimension": "255/40 R18 99V XL", - "manufacturer": "Pirelli", - "manufacturingWeek": "26 / 19", - "maxSpeed": "240 km/h", - "mountingDate": "11/12/2021", - "optimizedForOemBmw": "Yes", - "partNumber": "2461778", - "season": "Winter tires", - "treadDesign": "SOTTOZERO 3" - }, - "status": { - "currentPressure": 250, - "localizedCurrentPressure": "2.5 bar", - "localizedTargetPressure": "2.9 bar", - "targetPressure": 290, - "wear": 0 - } - } - }, - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "0721", - "status": { - "chargingProfile": { - "chargingControlType": "weeklyPlanner", - "chargingMode": "immediateCharging", - "chargingPreference": "noPreSelection", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": false, - "departureTimes": [ - { - "action": "deactivate", - "id": 1, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 2, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 3, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 4, - "timerWeekDays": [] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 0, - "minute": 0 - }, - "start": { - "hour": 0, - "minute": 0 - } - } - }, - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60117, - "state": "OK", - "title": "Tires" - }, - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "27138", - "mileage": 27138, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59757, - "state": "Locked", - "title": "Lock status" - }, - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors" - }, - { - "criticalness": "nonCritical", - "iconId": 59725, - "state": "Closed", - "title": "All windows" - }, - { - "criticalness": "nonCritical", - "iconId": 59706, - "state": "Closed", - "title": "Hood" - }, - { - "criticalness": "nonCritical", - "iconId": 59704, - "state": "Closed", - "title": "Trunk" - }, - { - "criticalness": "nonCritical", - "iconId": 59705, - "state": "Closed", - "title": "Sunroof" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59691, - "infoLabel": "Combined Range", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": null, - "levelUnits": null, - "levelValue": null, - "mainBarValue": 0, - "rangeIconId": 59691, - "rangeUnits": "km", - "rangeValue": "368", - "secondaryBarValue": 0, - "showsBar": false - }, - { - "barType": null, - "chargingStatusIndicatorType": "CHARGING", - "chargingStatusType": "CHARGING", - "chargingType": "charging", - "iconOpacity": "high", - "infoIconId": 59689, - "infoLabel": "100% at ~12:43 AM", - "isCircleIcon": true, - "isInaccurate": true, - "levelIconId": 59689, - "levelUnits": "%", - "levelValue": "83", - "mainBarValue": 83, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "23", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - }, - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": 59682, - "levelUnits": "%", - "levelValue": "83", - "mainBarValue": 83, - "rangeIconId": 59681, - "rangeUnits": "km", - "rangeValue": "345", - "secondaryBarValue": 0, - "showsBar": true - } - ], - "issues": {}, - "lastUpdatedAt": "2021-11-14T20:20:21Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Vehicle check" - }, - { - "criticalness": "nonCritical", - "iconId": 60447, - "id": "TireWearFront", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Tire service, front tires" - }, - { - "criticalness": "nonCritical", - "iconId": 60447, - "id": "TireWearRear", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Tire service, rear tires" - }, - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "some_road \u2022 duration \u2022 -- EUR", - "title": "Brake fluid" - } - ], - "timestampMessage": "Updated from vehicle 11/14/2021 09:20 PM" - }, - "telematicsUnit": "ATM02", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 201, - "green": 94, - "red": 40 - } - }, - "vin": "some_vin_G21", - "year": 2020 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-sessions_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-sessions_0.json deleted file mode 100644 index e28b736b787fb..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-sessions_0.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "chargingSessions": { - "chargingListState": "HAS_SESSIONS", - "numberOfSessions": "6", - "sessions": [ - { - "energyCharged": "~ 13 kWh", - "id": "2021-11-10T11:24:34Z_e51ab124", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road 2022 duration 2022 -- EUR", - "title": "Yesterday 11:24 AM" - }, - { - "energyCharged": "~ 11 kWh", - "id": "2021-11-08T16:53:02Z_e51ab124", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road 2022 duration 2022 -- EUR", - "title": "Monday 4:53 PM" - }, - { - "energyCharged": "~ 12 kWh", - "id": "2021-11-07T13:35:27Z_e51ab124", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road 2022 duration 2022 -- EUR", - "title": "Sunday 1:35 PM" - }, - { - "energyCharged": "~ 13 kWh", - "id": "2021-11-05T10:53:57Z_e51ab124", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road 2022 duration 2022 -- EUR", - "title": "Friday 10:53 AM" - }, - { - "energyCharged": "~ 10 kWh", - "id": "2021-11-03T10:00:47Z_e51ab124", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road 2022 duration 2022 -- EUR", - "title": "11/3/2021 10:00 AM" - }, - { - "energyCharged": "~ 12 kWh", - "id": "2021-11-02T12:31:45Z_e51ab124", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_road 2022 duration 2022 -- EUR", - "title": "11/2/2021 12:31 PM" - } - ], - "total": "~ 71 kWh" - }, - "datePicker": { - "endDate": "2021-11-11T09:09:49Z", - "selectedDate": "2021-11-10T11:24:34Z", - "startDate": "2021-08-16T19:31:45Z" - }, - "paginationInfo": {} -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-statistics_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-statistics_0.json deleted file mode 100644 index 7089ad36280bd..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/charging-statistics_0.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "description": "November 2021", - "optStateType": "OPT_IN_WITH_SESSIONS", - "statistics": { - "numberOfChargingSessions": 6, - "numberOfChargingSessionsSemantics": "mobile20chsChargingSessionNumberSemantics", - "symbol": "~", - "totalEnergyCharged": 71, - "totalEnergyChargedSemantics": "mobile20chsApproximatelyTotalChargedSemantics" - } -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/vehicles_v2_bmw_0.json deleted file mode 100644 index fbcadbc4d967c..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/G30/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,384 +0,0 @@ -[ - { - "a4aType": "NOT_SUPPORTED", - "bodyType": "G30", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "remote360": { - "isComingSoonEnabled": false, - "isDataPrivacyEnabled": false, - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true - }, - "remoteSoftwareUpgrade": { - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "speechThirdPartyAlexa": { - "executionMessage": "Activate Alexa now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [ - "WIFI_HOTSPOT_SERVICE" - ], - "driveTrain": "PLUGIN_HYBRID", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "S15A-20-07-532", - "exFactoryPUStep": "0720", - "headUnit": "MGU", - "hmiVersion": "id7", - "iStep": "S15A-21-03-550", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "530e", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 41, - "isChargerConnected": false, - "state": "NOT_CHARGING", - "type": "NOT_AVAILABLE" - }, - "checkControlMessages": [], - "climateControl": { - "activity": "INACTIVE" - }, - "combinedRange": { - "distance": { - "units": "KILOMETERS", - "value": 116 - } - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 116 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "OPEN", - "driverRear": "CLOSED", - "passengerFront": "OPEN", - "passengerRear": "CLOSED" - }, - "hood": "OPEN", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "OPEN", - "passengerRear": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 9 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 41, - "distance": { - "units": "KILOMETERS", - "value": 9 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 11 - }, - "fuelPercentage": { - "value": 28 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-11T08:58:53Z", - "originCountryISO": "IE", - "serviceRequired": [ - { - "dateTime": "2022-08-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 25000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2023-08-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2024-08-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 60000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - } - ], - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "0321", - "status": { - "chargingProfile": { - "chargingControlType": "weeklyPlanner", - "chargingMode": "immediateCharging", - "chargingPreference": "noPreSelection", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": true, - "departureTimes": [ - { - "action": "deactivate", - "id": 1, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 2, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 3, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 4, - "timerWeekDays": [] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 0, - "minute": 0 - }, - "start": { - "hour": 0, - "minute": 0 - } - } - }, - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "7991", - "mileage": 7991, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors and windows" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59691, - "infoLabel": "Combined Range", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": null, - "levelUnits": null, - "levelValue": null, - "mainBarValue": 0, - "rangeIconId": 59691, - "rangeUnits": "km", - "rangeValue": "116", - "secondaryBarValue": 0, - "showsBar": false - }, - { - "barType": null, - "chargingStatusIndicatorType": "DEFAULT", - "chargingStatusType": "DEFAULT", - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59694, - "infoLabel": "State of Charge", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": 59694, - "levelUnits": "%", - "levelValue": "41", - "mainBarValue": 41, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "9", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - }, - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": 59682, - "levelUnits": "%", - "levelValue": "28", - "mainBarValue": 28, - "rangeIconId": 59681, - "rangeUnits": "km", - "rangeValue": "107", - "secondaryBarValue": 0, - "showsBar": true - } - ], - "issues": {}, - "lastUpdatedAt": "2021-11-11T08:58:53Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in August 2022 or 25000 km", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in August 2023", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in August 2024 or 60000 km", - "title": "Vehicle check" - } - ], - "timestampMessage": "Updated from vehicle 11/12/2021 08:58 AM" - }, - "telematicsUnit": "ATM02", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 158, - "green": 158, - "red": 158 - } - }, - "vin": "some_vin_G30", - "year": 2020 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-sessions_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-sessions_0.json deleted file mode 100644 index a092c6366d435..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-sessions_0.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "chargingSessions": { - "chargingListState": "HAS_SESSIONS", - "numberOfSessions": "1", - "sessions": [ - { - "energyCharged": "~ 36 kWh", - "id": "2021-11-09T18:06:18Z_some_id", - "isPublic": false, - "sessionStatus": "FINISHED", - "subtitle": "some_place \u2022 3h 42min \u2022 -- EUR", - "title": "Tuesday 7:06 PM" - } - ], - "total": "~ 36 kWh" - }, - "datePicker": { - "endDate": "2021-11-11T00:44:47Z", - "selectedDate": "2021-11-09T18:06:18Z", - "startDate": "2021-08-08T15:51:27Z" - }, - "paginationInfo": {} -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-statistics_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-statistics_0.json deleted file mode 100644 index 8ca915abdfdbc..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/charging-statistics_0.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "description": "November 2021", - "optStateType": "OPT_IN_WITH_SESSIONS", - "statistics": { - "numberOfChargingSessions": 1, - "numberOfChargingSessionsSemantics": "mobile20chsChargingSessionNumberSemantics", - "symbol": "~", - "totalEnergyCharged": 36, - "totalEnergyChargedSemantics": "mobile20chsApproximatelyTotalChargedSemantics" - } -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/vehicles_v2_bmw_0.json deleted file mode 100644 index 80ba8a41387a7..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_NOREX/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,316 +0,0 @@ -[ - { - "a4aType": "BLUETOOTH", - "bodyType": "I01", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": true, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": true, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "speechThirdPartyAlexa": { - "executionMessage": "Activate Alexa now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [ - "WIFI_HOTSPOT_SERVICE" - ], - "driveTrain": "ELECTRIC", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "I001-20-11-520", - "exFactoryPUStep": "1120", - "headUnit": "ID5", - "hmiVersion": "ID5", - "iStep": "I001-20-11-520", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "i3 120", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 94, - "isChargerConnected": false, - "state": "NOT_CHARGING", - "type": "NOT_AVAILABLE" - }, - "checkControlMessages": [], - "climateControl": {}, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 0 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "passengerFront": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 229 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 94, - "distance": { - "units": "KILOMETERS", - "value": 229 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 0 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-10T18:18:05Z", - "originCountryISO": "DE", - "serviceRequired": [ - { - "dateTime": "2023-02-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2023-02-01T00:00:00.000Z", - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "dateTime": "2024-03-01T00:00:00.000Z", - "status": "OK", - "type": "VEHICLE_TUV" - } - ], - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "1120", - "status": { - "chargingProfile": { - "chargingControlType": "weeklyPlanner", - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": false, - "departureTimes": [ - { - "action": "deactivate", - "id": 1, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 2, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 3, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 4, - "timerWeekDays": [] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 0, - "minute": 0 - }, - "start": { - "hour": 0, - "minute": 0 - } - } - }, - "checkControlMessages": [], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "1250", - "mileage": 1250, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors and windows" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "barType": null, - "chargingStatusIndicatorType": "DEFAULT", - "chargingStatusType": "DEFAULT", - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59694, - "infoLabel": "State of Charge", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": 59694, - "levelUnits": "%", - "levelValue": "94", - "mainBarValue": 94, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "229", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - } - ], - "issues": {}, - "lastUpdatedAt": "2021-11-10T18:18:05Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in February 2023", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in February 2023", - "title": "Vehicle check" - }, - { - "criticalness": "nonCritical", - "iconId": 60111, - "id": "VehicleAdmissionTest", - "longDescription": "Next state inspection due by the specified date.", - "subtitle": "Due in March 2024", - "title": "Vehicle Inspection" - } - ], - "timestampMessage": "Updated from vehicle 11/10/2021 07:18 PM" - }, - "telematicsUnit": "ATM", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 152, - "green": 154, - "red": 156 - } - }, - "vin": "some_vin_I01_NOREX", - "year": 2021 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-sessions.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-sessions.json deleted file mode 100644 index 0ea498814ce89..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-sessions.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "paginationInfo": { - - }, - "chargingSessions": { - "total": "~ 218 kWh", - "numberOfSessions": "17", - "chargingListState": "HAS_SESSIONS", - "sessions": [ - { - "id": "2021-12-26T16:57:20Z_128fa4af", - "title": "Gestern 17:57", - "subtitle": "Uferstraße 4B • 7h 45min • -- EUR", - "energyCharged": "~ 31 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-26T16:02:49Z_128fa4af", - "title": "Gestern 17:02", - "subtitle": "Uferstraße 4C • 32 min • -- EUR", - "energyCharged": "~ 2 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-26T09:44:36Z_128fa4af", - "title": "Gestern 10:44", - "subtitle": "Kelzer Weg 24 • 58 min • -- EUR", - "energyCharged": "~ 2 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-25T22:18:46Z_128fa4af", - "title": "Samstag 23:18", - "subtitle": "Kelzer Weg 24 • 3h 42min • -- EUR", - "energyCharged": "~ 8 kWh", - "issues": "2 Probleme", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-24T11:56:09Z_128fa4af", - "title": "Freitag 12:56", - "subtitle": "Kelzer Weg 24 • 8h 46min • -- EUR", - "energyCharged": "~ 19 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-24T09:56:13Z_128fa4af", - "title": "Freitag 10:56", - "subtitle": "Kelzer Weg 15A • 1h 48min • -- EUR", - "energyCharged": "~ 4 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-23T07:55:12Z_128fa4af", - "title": "Donnerstag 08:55", - "subtitle": "Uferstraße 4C • 2h 55min • -- EUR", - "energyCharged": "~ 21 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-20T09:42:28Z_128fa4af", - "title": "20.12.2021 10:42", - "subtitle": "Hermannsteiner Straße 13 • 1h 14min", - "energyCharged": "~ 21 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-20T09:37:51Z_128fa4af", - "title": "20.12.2021 10:37", - "subtitle": "Hermannsteiner Straße 13 • < 1 min", - "energyCharged": "< 2 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-19T12:07:18Z_128fa4af", - "title": "19.12.2021 13:07", - "subtitle": "Uferstraße 4B • 2h 07min • -- EUR", - "energyCharged": "~ 9 kWh", - "issues": "1 Problem", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-18T10:56:31Z_128fa4af", - "title": "18.12.2021 11:56", - "subtitle": "Uferstraße 4C • 41 min • -- EUR", - "energyCharged": "~ 5 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-16T11:08:30Z_128fa4af", - "title": "16.12.2021 12:08", - "subtitle": "Uferstraße 4B • 2h 07min • -- EUR", - "energyCharged": "~ 9 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-15T12:38:23Z_128fa4af", - "title": "15.12.2021 13:38", - "subtitle": "Uferstraße 4C • 1h 55min • -- EUR", - "energyCharged": "~ 8 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-12T10:48:16Z_128fa4af", - "title": "12.12.2021 11:48", - "subtitle": "Uferstraße 4C • 6h 06min • -- EUR", - "energyCharged": "~ 23 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-07T16:09:02Z_128fa4af", - "title": "07.12.2021 17:09", - "subtitle": "Uferstraße 4B • 5h 40min • -- EUR", - "energyCharged": "~ 21 kWh", - "issues": "1 Problem", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-04T09:51:23Z_128fa4af", - "title": "04.12.2021 10:51", - "subtitle": "L3053 • 1h 24min", - "energyCharged": "~ 22 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - }, - { - "id": "2021-12-02T13:42:28Z_128fa4af", - "title": "02.12.2021 14:42", - "subtitle": "Uferstraße 4C • 2h 29min • -- EUR", - "energyCharged": "~ 11 kWh", - "sessionStatus": "FINISHED", - "isPublic": false - } - ] - }, - "datePicker": { - "startDate": "2020-11-07T09:58:20Z", - "selectedDate": "2021-12-26T16:57:20Z", - "endDate": "2021-12-27T16:10:53Z" - } -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-de.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-de.json deleted file mode 100644 index e368f22874a31..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-de.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "description": "Dezember 2021", - "optStateType": "OPT_IN_WITH_SESSIONS", - "statistics": { - "totalEnergyCharged": 173, - "totalEnergyChargedSemantics": "Insgesamt circa 173 Kilowattstunden geladen", - "symbol": "~", - "numberOfChargingSessions": 13, - "numberOfChargingSessionsSemantics": "13 Ladevorgänge" - } -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-en.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-en.json deleted file mode 100644 index a75fb1d1b894d..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charge-statistics-en.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "description": "December 2021", - "optStateType": "OPT_IN_WITH_SESSIONS", - "statistics": { - "totalEnergyCharged": 173, - "totalEnergyChargedSemantics": "Charged a total of approximately 173 kilowatt-hours", - "symbol": "~", - "numberOfChargingSessions": 13, - "numberOfChargingSessionsSemantics": "13 charging sessions" - } -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charging-statistics_1.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charging-statistics_1.json deleted file mode 100644 index 978aeca660800..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/charging-statistics_1.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "description": "November 2021", - "optStateType": "OPT_IN_WITH_SESSIONS", - "statistics": { - "numberOfChargingSessions": 15, - "numberOfChargingSessionsSemantics": "mobile20chsChargingSessionNumberSemantics", - "symbol": "~", - "totalEnergyCharged": 144, - "totalEnergyChargedSemantics": "mobile20chsApproximatelyTotalChargedSemantics" - } -} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-charging.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-charging.json deleted file mode 100644 index 4fb246173f870..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-charging.json +++ /dev/null @@ -1,427 +0,0 @@ -[ - { - "vin": "anonymous", - "model": "i3 94 (+ REX)", - "year": 2017, - "brand": "BMW", - "headUnit": "ID5", - "isLscSupported": true, - "driveTrain": "ELECTRIC", - "puStep": "0321", - "iStep": "I001-21-03-530", - "telematicsUnit": "TCB1", - "hmiVersion": "ID4", - "bodyType": "I01", - "a4aType": "USB_ONLY", - "capabilities": { - "isRemoteServicesBookingRequired": false, - "isRemoteServicesActivationRequired": false, - "lock": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds." - }, - "unlock": { - "isEnabled": true, - "isPinAuthenticationRequired": true, - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds." - }, - "lights": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Flash headlights now? Remote functions may take a few seconds." - }, - "horn": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds." - }, - "vehicleFinder": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds." - }, - "sendPoi": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Send POI now? Remote functions may take a few seconds." - }, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "climateNow": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "popupType": "DIALOG", - "title": "Start Climatization", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "iconId": 59733 - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - } - }, - "isRemoteHistorySupported": true, - "canRemoteHistoryBeDeleted": false, - "isChargingHistorySupported": true, - "isScanAndChargeSupported": true, - "isDCSContractManagementSupported": true, - "isBmwChargingSupported": true, - "isMiniChargingSupported": false, - "isChargeNowForBusinessSupported": true, - "isDataPrivacyEnabled": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingTargetSocEnable": false, - "isChargingLoudnessEnable": false, - "isChargingSettingsEnabled": false, - "isChargingHospitalityEnabled": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isCustomerEsimSupported": false, - "isCarSharingSupported": false, - "isEasyChargeSupported": false - }, - "connectedDriveServices": [], - "properties": { - "lastUpdatedAt": "2021-12-25T22:29:22Z", - "inMotion": false, - "areDoorsLocked": true, - "originCountryISO": "DE", - "areDoorsClosed": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "windows": { - "driverFront": "CLOSED", - "passengerFront": "CLOSED" - }, - "trunk": "CLOSED", - "hood": "CLOSED", - "moonroof": "CLOSED" - }, - "isServiceRequired": false, - "fuelLevel": { - "value": 9, - "units": "LITERS" - }, - "chargingState": { - "chargePercentage": 86, - "state": "CHARGING", - "type": "NOT_AVAILABLE", - "isChargerConnected": true - }, - "combustionRange": { - "distance": { - "value": 97, - "units": "KILOMETERS" - } - }, - "combinedRange": { - "distance": { - "value": 97, - "units": "KILOMETERS" - } - }, - "electricRange": { - "distance": { - "value": 121, - "units": "KILOMETERS" - } - }, - "electricRangeAndStatus": { - "chargePercentage": 86, - "distance": { - "value": 121, - "units": "KILOMETERS" - } - }, - "checkControlMessages": [], - "serviceRequired": [ - { - "type": "BRAKE_FLUID", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_CHECK", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "OIL", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_TUV", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - } - ], - "vehicleLocation": { - "coordinates": { - "latitude": 1.234, - "longitude": 9.876 - }, - "address": { - "formatted": "anonymous" - }, - "heading": 73 - }, - "climateControl": { - - } - }, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "driverGuideInfo": { - "title": "BMW\nDriver's Guide", - "androidAppScheme": "com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" - }, - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "red": 156, - "green": 154, - "blue": 152 - } - }, - "status": { - "lastUpdatedAt": "2021-12-25T22:29:22Z", - "currentMileage": { - "mileage": 31746, - "units": "km", - "formattedMileage": "31746" - }, - "issues": { - - }, - "doorsGeneralState": "Locked", - "checkControlMessagesGeneralState": "No Issues", - "doorsAndWindows": [ - { - "iconId": 59757, - "title": "Lock status", - "state": "Locked", - "criticalness": "nonCritical" - }, - { - "iconId": 59722, - "title": "All doors", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59725, - "title": "All windows", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59706, - "title": "Hood", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59704, - "title": "Trunk", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59705, - "title": "Sunroof", - "state": "Closed", - "criticalness": "nonCritical" - } - ], - "checkControlMessages": [], - "requiredServices": [ - { - "id": "BrakeFluid", - "title": "Brake fluid", - "iconId": 60223, - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleCheck", - "title": "Vehicle check", - "iconId": 60215, - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - }, - { - "id": "Oil", - "title": "Engine oil", - "iconId": 60197, - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleAdmissionTest", - "title": "Vehicle Inspection", - "iconId": 60111, - "longDescription": "Next state inspection due by the specified date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - } - ], - "recallMessages": [], - "recallExternalUrl": null, - "fuelIndicators": [ - { - "mainBarValue": 86, - "secondaryBarValue": 0, - "infoIconId": 59689, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "121", - "levelIconId": 59689, - "showsBar": true, - "levelUnits": "%", - "levelValue": "86", - "showBarGoal": false, - "barType": null, - "infoLabel": "100% at ~02:59 AM", - "isInaccurate": true, - "isCircleIcon": true, - "iconOpacity": "high", - "chargingType": "charging", - "chargingStatusType": "CHARGING", - "chargingStatusIndicatorType": "CHARGING" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59691, - "infoLabel": "Combined Range", - "rangeIconId": 59691, - "rangeUnits": "km", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null, - "rangeValue": "218" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59681, - "infoLabel": "Extended Range", - "rangeIconId": null, - "rangeUnits": "km", - "rangeValue": "97", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null - } - ], - "timestampMessage": "Updated from vehicle 12/25/2021 11:29 PM", - "chargingProfile": { - "reductionOfChargeCurrent": { - "start": { - "hour": 11, - "minute": 0 - }, - "end": { - "hour": 14, - "minute": 30 - } - }, - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingControlType": "weeklyPlanner", - "departureTimes": [ - { - "id": 1, - "action": "deactivate", - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "timeStamp": { - "hour": 16, - "minute": 0 - } - }, - { - "id": 2, - "action": "activate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - }, - { - "id": 3, - "action": "deactivate", - "timerWeekDays": [ - "saturday" - ], - "timeStamp": { - "hour": 13, - "minute": 3 - } - }, - { - "id": 4, - "action": "deactivate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - } - ], - "climatisationOn": false, - "chargingSettings": { - "targetSoc": 100, - "isAcCurrentLimitActive": false, - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION" - } - } - }, - "exFactoryPUStep": "0717", - "exFactoryILevel": "I001-17-07-500" - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-fully-charged.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-fully-charged.json deleted file mode 100644 index cf524ebb9c644..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicle-fully-charged.json +++ /dev/null @@ -1,423 +0,0 @@ - [ - { - "vin": "anonymous", - "model": "i3 94 (+ REX)", - "year": 2017, - "brand": "BMW", - "headUnit": "ID5", - "isLscSupported": true, - "driveTrain": "ELECTRIC", - "puStep": "0321", - "iStep": "I001-21-03-530", - "telematicsUnit": "TCB1", - "hmiVersion": "ID4", - "bodyType": "I01", - "a4aType": "USB_ONLY", - "capabilities": { - "isRemoteServicesBookingRequired": false, - "isRemoteServicesActivationRequired": false, - "lock": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "unlock": { - "isEnabled": true, - "isPinAuthenticationRequired": true, - "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "lights": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern." - }, - "horn": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern." - }, - "vehicleFinder": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern." - }, - "sendPoi": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern." - }, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "climateNow": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern.", - "executionPopup": { - "executionMessage": "Jetzt klimatisieren? Remote-Funktionen können einige Sekunden dauern.", - "popupType": "DIALOG", - "title": "Klimatisierung starten", - "primaryButtonText": "Start", - "secondaryButtonText": "Abbrechen", - "iconId": 59733 - }, - "executionStopPopup": { - "executionMessage": "Jetzt Klimatisierung Ihres Fahrzeugs beenden? Remote-Funktionen können einige Sekunden dauern.", - "title": "Klimatisierung läuft" - } - }, - "isRemoteHistorySupported": true, - "canRemoteHistoryBeDeleted": false, - "isChargingHistorySupported": true, - "isScanAndChargeSupported": true, - "isDCSContractManagementSupported": true, - "isBmwChargingSupported": true, - "isMiniChargingSupported": false, - "isChargeNowForBusinessSupported": true, - "isDataPrivacyEnabled": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingTargetSocEnable": false, - "isChargingLoudnessEnable": false, - "isChargingSettingsEnabled": false, - "isChargingHospitalityEnabled": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isCustomerEsimSupported": false, - "isCarSharingSupported": false, - "isEasyChargeSupported": false - }, - "connectedDriveServices": [], - "properties": { - "lastUpdatedAt": "2022-01-04T21:04:49Z", - "inMotion": false, - "areDoorsLocked": true, - "originCountryISO": "DE", - "areDoorsClosed": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "windows": { - "driverFront": "CLOSED", - "passengerFront": "CLOSED" - }, - "trunk": "CLOSED", - "hood": "CLOSED", - "moonroof": "CLOSED" - }, - "isServiceRequired": false, - "fuelLevel": { - "value": 7, - "units": "LITERS" - }, - "chargingState": { - "chargePercentage": 100, - "state": "COMPLETE", - "type": "NOT_AVAILABLE", - "isChargerConnected": true - }, - "combustionRange": { - "distance": { - "value": 90, - "units": "KILOMETERS" - } - }, - "combinedRange": { - "distance": { - "value": 90, - "units": "KILOMETERS" - } - }, - "electricRange": { - "distance": { - "value": 162, - "units": "KILOMETERS" - } - }, - "electricRangeAndStatus": { - "chargePercentage": 100, - "distance": { - "value": 162, - "units": "KILOMETERS" - } - }, - "checkControlMessages": [], - "serviceRequired": [ - { - "type": "BRAKE_FLUID", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_CHECK", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "OIL", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_TUV", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - } - ], - "vehicleLocation": { - "coordinates": { - "latitude": 1.2345, - "longitude": 9.876 - }, - "address": { - "formatted": "anonymous" - }, - "heading": 39 - }, - "climateControl": null - }, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "driverGuideInfo": { - "title": "BMW\nDriver's Guide", - "androidAppScheme": "com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" - }, - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "red": 156, - "green": 154, - "blue": 152 - } - }, - "status": { - "lastUpdatedAt": "2022-01-04T21:04:49Z", - "currentMileage": { - "mileage": 32219, - "units": "km", - "formattedMileage": "32.219" - }, - "issues": null, - "doorsGeneralState": "Verriegelt", - "checkControlMessagesGeneralState": "Keine Probleme", - "doorsAndWindows": [ - { - "iconId": 59757, - "title": "Verriegelungsstatus", - "state": "Verriegelt", - "criticalness": "nonCritical" - }, - { - "iconId": 59722, - "title": "Alle Türen", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59725, - "title": "Alle Fenster", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59706, - "title": "Frontklappe", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59704, - "title": "Gepäckraum", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59705, - "title": "Glasdach", - "state": "Geschlossen", - "criticalness": "nonCritical" - } - ], - "checkControlMessages": [], - "requiredServices": [ - { - "id": "BrakeFluid", - "title": "Bremsflüssigkeit", - "iconId": 60223, - "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleCheck", - "title": "Fahrzeug-Check", - "iconId": 60215, - "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "Oil", - "title": "Motoröl", - "iconId": 60197, - "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleAdmissionTest", - "title": "Fahrzeuginspektion (HU)", - "iconId": 60111, - "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - } - ], - "recallMessages": [], - "recallExternalUrl": null, - "fuelIndicators": [ - { - "mainBarValue": 100, - "secondaryBarValue": 0, - "infoIconId": 59689, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "162", - "levelIconId": 59689, - "showsBar": true, - "levelUnits": "%", - "levelValue": "100", - "showBarGoal": false, - "barType": null, - "infoLabel": "Voll geladen", - "isInaccurate": false, - "isCircleIcon": true, - "iconOpacity": "high", - "chargingType": "charging_complete", - "chargingStatusType": "FULLY_CHARGED", - "chargingStatusIndicatorType": "FULLY_CHARGED" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59691, - "infoLabel": "Kombinierte Reichweite", - "rangeIconId": 59691, - "rangeUnits": "km", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null, - "rangeValue": "252" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59681, - "infoLabel": "Erweiterte Reichweite", - "rangeIconId": null, - "rangeUnits": "km", - "rangeValue": "90", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null - } - ], - "timestampMessage": "Aktualisiert vom Fahrzeug 4.1.2022 10:04 PM", - "chargingProfile": { - "reductionOfChargeCurrent": { - "start": { - "hour": 11, - "minute": 0 - }, - "end": { - "hour": 14, - "minute": 30 - } - }, - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingControlType": "weeklyPlanner", - "departureTimes": [ - { - "id": 1, - "action": "deactivate", - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "timeStamp": { - "hour": 16, - "minute": 0 - } - }, - { - "id": 2, - "action": "activate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - }, - { - "id": 3, - "action": "deactivate", - "timerWeekDays": [ - "saturday" - ], - "timeStamp": { - "hour": 13, - "minute": 3 - } - }, - { - "id": 4, - "action": "deactivate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - } - ], - "climatisationOn": false, - "chargingSettings": { - "targetSoc": 100, - "isAcCurrentLimitActive": false, - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION" - } - } - }, - "exFactoryPUStep": "0717", - "exFactoryILevel": "I001-17-07-500" - } -] diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles-de.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles-de.json deleted file mode 100644 index 3be2f71d756a1..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles-de.json +++ /dev/null @@ -1,427 +0,0 @@ -[ - { - "vin": "anonymous", - "model": "i3 94 (+ REX)", - "year": 2017, - "brand": "BMW", - "headUnit": "ID5", - "isLscSupported": true, - "driveTrain": "ELECTRIC", - "puStep": "0321", - "iStep": "I001-21-03-530", - "telematicsUnit": "TCB1", - "hmiVersion": "ID4", - "bodyType": "I01", - "a4aType": "USB_ONLY", - "capabilities": { - "isRemoteServicesBookingRequired": false, - "isRemoteServicesActivationRequired": false, - "lock": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "unlock": { - "isEnabled": true, - "isPinAuthenticationRequired": true, - "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "lights": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern." - }, - "horn": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern." - }, - "vehicleFinder": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern." - }, - "sendPoi": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern." - }, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "climateNow": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern.", - "executionPopup": { - "executionMessage": "Jetzt klimatisieren? Remote-Funktionen können einige Sekunden dauern.", - "popupType": "DIALOG", - "title": "Klimatisierung starten", - "primaryButtonText": "Start", - "secondaryButtonText": "Abbrechen", - "iconId": 59733 - }, - "executionStopPopup": { - "executionMessage": "Jetzt Klimatisierung Ihres Fahrzeugs beenden? Remote-Funktionen können einige Sekunden dauern.", - "title": "Klimatisierung läuft" - } - }, - "isRemoteHistorySupported": true, - "canRemoteHistoryBeDeleted": false, - "isChargingHistorySupported": true, - "isScanAndChargeSupported": true, - "isDCSContractManagementSupported": true, - "isBmwChargingSupported": true, - "isMiniChargingSupported": false, - "isChargeNowForBusinessSupported": true, - "isDataPrivacyEnabled": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingTargetSocEnable": false, - "isChargingLoudnessEnable": false, - "isChargingSettingsEnabled": false, - "isChargingHospitalityEnabled": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isCustomerEsimSupported": false, - "isCarSharingSupported": false, - "isEasyChargeSupported": false - }, - "connectedDriveServices": [], - "properties": { - "lastUpdatedAt": "2021-12-26T09:56:05Z", - "inMotion": false, - "areDoorsLocked": true, - "originCountryISO": "DE", - "areDoorsClosed": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "windows": { - "driverFront": "CLOSED", - "passengerFront": "CLOSED" - }, - "trunk": "CLOSED", - "hood": "CLOSED", - "moonroof": "CLOSED" - }, - "isServiceRequired": false, - "fuelLevel": { - "value": 9, - "units": "LITERS" - }, - "chargingState": { - "chargePercentage": 100, - "state": "CHARGING", - "type": "NOT_AVAILABLE", - "isChargerConnected": true - }, - "combustionRange": { - "distance": { - "value": 98, - "units": "KILOMETERS" - } - }, - "combinedRange": { - "distance": { - "value": 98, - "units": "KILOMETERS" - } - }, - "electricRange": { - "distance": { - "value": 146, - "units": "KILOMETERS" - } - }, - "electricRangeAndStatus": { - "chargePercentage": 100, - "distance": { - "value": 146, - "units": "KILOMETERS" - } - }, - "checkControlMessages": [], - "serviceRequired": [ - { - "type": "BRAKE_FLUID", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_CHECK", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "OIL", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_TUV", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - } - ], - "vehicleLocation": { - "coordinates": { - "latitude": 1.234, - "longitude": 5.678 - }, - "address": { - "formatted": "where-ever" - }, - "heading": 73 - }, - "climateControl": { - - } - }, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "driverGuideInfo": { - "title": "BMW\nDriver's Guide", - "androidAppScheme": "com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" - }, - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "red": 156, - "green": 154, - "blue": 152 - } - }, - "status": { - "lastUpdatedAt": "2021-12-26T09:56:05Z", - "currentMileage": { - "mileage": 31746, - "units": "km", - "formattedMileage": "31.746" - }, - "issues": { - - }, - "doorsGeneralState": "Verriegelt", - "checkControlMessagesGeneralState": "Keine Probleme", - "doorsAndWindows": [ - { - "iconId": 59757, - "title": "Verriegelungsstatus", - "state": "Verriegelt", - "criticalness": "nonCritical" - }, - { - "iconId": 59722, - "title": "Alle Türen", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59725, - "title": "Alle Fenster", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59706, - "title": "Frontklappe", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59704, - "title": "Gepäckraum", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59705, - "title": "Glasdach", - "state": "Geschlossen", - "criticalness": "nonCritical" - } - ], - "checkControlMessages": [], - "requiredServices": [ - { - "id": "BrakeFluid", - "title": "Bremsflüssigkeit", - "iconId": 60223, - "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleCheck", - "title": "Fahrzeug-Check", - "iconId": 60215, - "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "Oil", - "title": "Motoröl", - "iconId": 60197, - "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleAdmissionTest", - "title": "Fahrzeuginspektion (HU)", - "iconId": 60111, - "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - } - ], - "recallMessages": [], - "recallExternalUrl": null, - "fuelIndicators": [ - { - "mainBarValue": 100, - "secondaryBarValue": 0, - "infoIconId": 59689, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "146", - "levelIconId": 59689, - "showsBar": true, - "levelUnits": "%", - "levelValue": "100", - "showBarGoal": false, - "barType": null, - "infoLabel": "um ~11:21 AM", - "isInaccurate": true, - "isCircleIcon": true, - "iconOpacity": "high", - "chargingType": "charging", - "chargingStatusType": "CHARGING", - "chargingStatusIndicatorType": "CHARGING" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59691, - "infoLabel": "Kombinierte Reichweite", - "rangeIconId": 59691, - "rangeUnits": "km", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null, - "rangeValue": "244" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59681, - "infoLabel": "Erweiterte Reichweite", - "rangeIconId": null, - "rangeUnits": "km", - "rangeValue": "98", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null - } - ], - "timestampMessage": "Aktualisiert vom Fahrzeug 26.12.2021 10:56 AM", - "chargingProfile": { - "reductionOfChargeCurrent": { - "start": { - "hour": 11, - "minute": 0 - }, - "end": { - "hour": 14, - "minute": 30 - } - }, - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingControlType": "weeklyPlanner", - "departureTimes": [ - { - "id": 1, - "action": "deactivate", - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "timeStamp": { - "hour": 16, - "minute": 0 - } - }, - { - "id": 2, - "action": "activate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - }, - { - "id": 3, - "action": "deactivate", - "timerWeekDays": [ - "saturday" - ], - "timeStamp": { - "hour": 13, - "minute": 3 - } - }, - { - "id": 4, - "action": "deactivate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - } - ], - "climatisationOn": false, - "chargingSettings": { - "targetSoc": 100, - "isAcCurrentLimitActive": false, - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION" - } - } - }, - "exFactoryPUStep": "0717", - "exFactoryILevel": "I001-17-07-500" - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles.json deleted file mode 100644 index 15afdbad8ca54..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles.json +++ /dev/null @@ -1,427 +0,0 @@ -[ - { - "vin": "anonymous", - "model": "i3 94 (+ REX)", - "year": 2017, - "brand": "BMW", - "headUnit": "ID5", - "isLscSupported": true, - "driveTrain": "ELECTRIC", - "puStep": "0321", - "iStep": "I001-21-03-530", - "telematicsUnit": "TCB1", - "hmiVersion": "ID4", - "bodyType": "I01", - "a4aType": "USB_ONLY", - "capabilities": { - "isRemoteServicesBookingRequired": false, - "isRemoteServicesActivationRequired": false, - "lock": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds." - }, - "unlock": { - "isEnabled": true, - "isPinAuthenticationRequired": true, - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds." - }, - "lights": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Flash headlights now? Remote functions may take a few seconds." - }, - "horn": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds." - }, - "vehicleFinder": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds." - }, - "sendPoi": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Send POI now? Remote functions may take a few seconds." - }, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "climateNow": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "popupType": "DIALOG", - "title": "Start Climatization", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "iconId": 59733 - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - } - }, - "isRemoteHistorySupported": true, - "canRemoteHistoryBeDeleted": false, - "isChargingHistorySupported": true, - "isScanAndChargeSupported": true, - "isDCSContractManagementSupported": true, - "isBmwChargingSupported": true, - "isMiniChargingSupported": false, - "isChargeNowForBusinessSupported": true, - "isDataPrivacyEnabled": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingTargetSocEnable": false, - "isChargingLoudnessEnable": false, - "isChargingSettingsEnabled": false, - "isChargingHospitalityEnabled": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isCustomerEsimSupported": false, - "isCarSharingSupported": false, - "isEasyChargeSupported": false - }, - "connectedDriveServices": [], - "properties": { - "lastUpdatedAt": "2021-12-21T16:46:02Z", - "inMotion": false, - "areDoorsLocked": true, - "originCountryISO": "DE", - "areDoorsClosed": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "windows": { - "driverFront": "CLOSED", - "passengerFront": "CLOSED" - }, - "trunk": "CLOSED", - "hood": "CLOSED", - "moonroof": "CLOSED" - }, - "isServiceRequired": false, - "fuelLevel": { - "value": 4, - "units": "LITERS" - }, - "chargingState": { - "chargePercentage": 74, - "state": "NOT_CHARGING", - "type": "NOT_AVAILABLE", - "isChargerConnected": false - }, - "combustionRange": { - "distance": { - "value": 31, - "units": "KILOMETERS" - } - }, - "combinedRange": { - "distance": { - "value": 31, - "units": "KILOMETERS" - } - }, - "electricRange": { - "distance": { - "value": 76, - "units": "KILOMETERS" - } - }, - "electricRangeAndStatus": { - "chargePercentage": 74, - "distance": { - "value": 76, - "units": "KILOMETERS" - } - }, - "checkControlMessages": [], - "serviceRequired": [ - { - "type": "BRAKE_FLUID", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_CHECK", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "OIL", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_TUV", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - } - ], - "vehicleLocation": { - "coordinates": { - "latitude": 54.321, - "longitude": 9.876 - }, - "address": { - "formatted": "anonymous" - }, - "heading": 222 - }, - "climateControl": { - - } - }, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "driverGuideInfo": { - "title": "BMW\nDriver's Guide", - "androidAppScheme": "com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" - }, - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "red": 156, - "green": 154, - "blue": 152 - } - }, - "status": { - "lastUpdatedAt": "2021-12-21T16:46:02Z", - "currentMileage": { - "mileage": 31537, - "units": "km", - "formattedMileage": "31537" - }, - "issues": { - - }, - "doorsGeneralState": "Locked", - "checkControlMessagesGeneralState": "No Issues", - "doorsAndWindows": [ - { - "iconId": 59757, - "title": "Lock status", - "state": "Locked", - "criticalness": "nonCritical" - }, - { - "iconId": 59722, - "title": "All doors", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59725, - "title": "All windows", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59706, - "title": "Hood", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59704, - "title": "Trunk", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59705, - "title": "Sunroof", - "state": "Closed", - "criticalness": "nonCritical" - } - ], - "checkControlMessages": [], - "requiredServices": [ - { - "id": "BrakeFluid", - "title": "Brake fluid", - "iconId": 60223, - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleCheck", - "title": "Vehicle check", - "iconId": 60215, - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - }, - { - "id": "Oil", - "title": "Engine oil", - "iconId": 60197, - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleAdmissionTest", - "title": "Vehicle Inspection", - "iconId": 60111, - "longDescription": "Next state inspection due by the specified date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - } - ], - "recallMessages": [], - "recallExternalUrl": null, - "fuelIndicators": [ - { - "mainBarValue": 74, - "secondaryBarValue": 0, - "infoIconId": 59694, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "76", - "levelIconId": 59694, - "showsBar": true, - "levelUnits": "%", - "levelValue": "74", - "showBarGoal": false, - "barType": null, - "infoLabel": "State of Charge", - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null, - "chargingStatusType": "DEFAULT", - "chargingStatusIndicatorType": "DEFAULT" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59691, - "infoLabel": "Combined Range", - "rangeIconId": 59691, - "rangeUnits": "km", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null, - "rangeValue": "107" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59681, - "infoLabel": "Extended Range", - "rangeIconId": null, - "rangeUnits": "km", - "rangeValue": "31", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null - } - ], - "timestampMessage": "Updated from vehicle 12/21/2021 05:46 PM", - "chargingProfile": { - "reductionOfChargeCurrent": { - "start": { - "hour": 11, - "minute": 0 - }, - "end": { - "hour": 14, - "minute": 30 - } - }, - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingControlType": "weeklyPlanner", - "departureTimes": [ - { - "id": 1, - "action": "deactivate", - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "timeStamp": { - "hour": 16, - "minute": 0 - } - }, - { - "id": 2, - "action": "activate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - }, - { - "id": 3, - "action": "deactivate", - "timerWeekDays": [ - "saturday" - ], - "timeStamp": { - "hour": 13, - "minute": 3 - } - }, - { - "id": 4, - "action": "deactivate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - } - ], - "climatisationOn": false, - "chargingSettings": { - "targetSoc": 100, - "isAcCurrentLimitActive": false, - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION" - } - } - }, - "exFactoryPUStep": "0717", - "exFactoryILevel": "I001-17-07-500" - } -] diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json deleted file mode 100644 index f8d79739cea58..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/I01_REX/vehicles_v2_bmw_0.json +++ /dev/null @@ -1,387 +0,0 @@ -[ - { - "a4aType": "USB_ONLY", - "bodyType": "I01", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "horn": { - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [], - "driveTrain": "ELECTRIC", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "I001-15-03-502", - "exFactoryPUStep": "0315", - "headUnit": "ID5", - "hmiVersion": "ID4", - "iStep": "I001-21-03-530", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "i3 (+ REX)", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": false, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 100, - "isChargerConnected": true, - "state": "COMPLETE", - "type": "CONDUCTIVE" - }, - "checkControlMessages": [], - "climateControl": {}, - "combinedRange": { - "distance": { - "units": "KILOMETERS", - "value": 64 - } - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 64 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "moonroof": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "passengerFront": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 164 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 100, - "distance": { - "units": "KILOMETERS", - "value": 164 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 5 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-11T06:49:47Z", - "originCountryISO": "CZ", - "serviceRequired": [ - { - "dateTime": "2022-10-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2023-05-01T00:00:00.000Z", - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "dateTime": "2023-05-01T00:00:00.000Z", - "status": "OK", - "type": "VEHICLE_TUV" - } - ], - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "0321", - "status": { - "chargingProfile": { - "chargingControlType": "weeklyPlanner", - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": true, - "departureTimes": [ - { - "action": "activate", - "id": 1, - "timeStamp": { - "hour": 7, - "minute": 35 - }, - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday" - ] - }, - { - "action": "deactivate", - "id": 2, - "timeStamp": { - "hour": 18, - "minute": 0 - }, - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ] - }, - { - "action": "deactivate", - "id": 3, - "timeStamp": { - "hour": 7, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 4, - "timeStamp": { - "hour": 7, - "minute": 35 - }, - "timerWeekDays": [ - "friday" - ] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 1, - "minute": 30 - }, - "start": { - "hour": 18, - "minute": 1 - } - } - }, - "checkControlMessages": [], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "124462", - "mileage": 124462, - "units": "km" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors and windows" - } - ], - "doorsGeneralState": "Unlocked", - "fuelIndicators": [ - { - "barType": null, - "chargingStatusIndicatorType": "FULLY_CHARGED", - "chargingStatusType": "FULLY_CHARGED", - "chargingType": "charging_complete", - "iconOpacity": "high", - "infoIconId": 59689, - "infoLabel": "Fully Charged", - "isCircleIcon": true, - "isInaccurate": false, - "levelIconId": 59689, - "levelUnits": "%", - "levelValue": "100", - "mainBarValue": 100, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "164", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - }, - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59691, - "infoLabel": "Combined Range", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": null, - "levelUnits": null, - "levelValue": null, - "mainBarValue": 0, - "rangeIconId": 59691, - "rangeUnits": "km", - "rangeValue": "228", - "secondaryBarValue": 0, - "showsBar": false - }, - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59681, - "infoLabel": "Extended Range", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": null, - "levelUnits": null, - "levelValue": null, - "mainBarValue": 0, - "rangeIconId": null, - "rangeUnits": "km", - "rangeValue": "64", - "secondaryBarValue": 0, - "showsBar": false - } - ], - "issues": { - "doorsAndWindows": { - "iconId": 59737, - "title": "Vehicle unlocked" - } - }, - "lastUpdatedAt": "2021-11-11T06:49:47Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in October 2022", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in May 2023", - "title": "Vehicle check" - }, - { - "criticalness": "nonCritical", - "iconId": 60111, - "id": "VehicleAdmissionTest", - "longDescription": "Next state inspection due by the specified date.", - "subtitle": "Due in May 2023", - "title": "Vehicle Inspection" - } - ], - "timestampMessage": "Updated from vehicle 11/11/2021 07:49 AM" - }, - "telematicsUnit": "TCB1", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 86, - "green": 88, - "red": 90 - } - }, - "vin": "some_vin_I01_REX", - "year": 2015 - } -] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/charging_sessions.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/charging_sessions.json new file mode 100644 index 0000000000000..47c9cbddb05ae --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/charging_sessions.json @@ -0,0 +1,14 @@ +{ + "chargingSessions": { + "total": "0 kWh", + "numberOfSessions": "0", + "emptyStateDescription": "Charge your BMW and track your charging history.\nSession data collected from 7/27/2020 on, your activation date.", + "chargingListState": "CHARGE_AND_TRACK", + "costsGroupedByCurrency": [] + }, + "datePicker": { + "startDate": "2020-07-27T00:00:00Z", + "selectedDate": "2023-01-21T18:57:42Z", + "endDate": "2023-01-21T18:57:43Z" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/charging_statistics.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/charging_statistics.json new file mode 100644 index 0000000000000..321efde21e348 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/charging_statistics.json @@ -0,0 +1,8 @@ +{ + "description": "Charge your BMW and track your charging history. Session data collected from 1/1/0001 on, your activation date.", + "optStateType": "OPT_IN_WITHOUT_SESSIONS", + "statistics": { + "totalEnergyCharged": 0, + "numberOfChargingSessions": 0 + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/vehicles_base.json new file mode 100644 index 0000000000000..c77c559a52763 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/vehicles_base.json @@ -0,0 +1,48 @@ +[ + { + "vin": "anonymousICE", + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "mappingStatus": "CONFIRMED", + "isPrimaryUser": true + }, + "appVehicleType": "CONNECTED", + "attributes": { + "lastFetched": "2022-12-22T18:58:29.700Z", + "model": "Cooper", + "year": 2022, + "color": 4290295992, + "brand": "MINI", + "driveTrain": "COMBUSTION", + "headUnitType": "ENTRY_EVO", + "headUnitRaw": "ENAVEVO", + "hmiVersion": "ID5", + "softwareVersionCurrent": { + "puStep": { + "month": 3, + "year": 22 + }, + "iStep": 580, + "seriesCluster": "F056" + }, + "softwareVersionExFactory": { + "puStep": { + "month": 3, + "year": 22 + }, + "iStep": 580, + "seriesCluster": "F056" + }, + "telematicsUnit": "ATM1", + "bodyType": "F56", + "countryOfOrigin": "DE", + "driverGuideInfo": { + "androidAppScheme": "com.mini.driversguide.row", + "iosAppScheme": "minidriversguide:///open", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.mini.driversguide.row", + "iosStoreUrl": "https://apps.apple.com/de/app/id834510424?mt=8" + } + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/vehicles_state.json new file mode 100644 index 0000000000000..c921334b261e7 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE/vehicles_state.json @@ -0,0 +1,166 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2023-02-01T21:30:24.651Z", + "lastUpdatedAt": "2023-02-01T21:23:36Z", + "isLscSupported": true, + "range": 123, + "doorsState": { + "combinedSecurityState": "PARTIALLY_LOCKED", + "leftFront": "CLOSED", + "rightFront": "CLOSED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "rightFront": "CLOSED", + "combinedState": "CLOSED" + }, + "location": { + "coordinates": { + "latitude": 1.23, + "longitude": 3.45 + }, + "address": { + "formatted": "anonymousAddress" + }, + "heading": 173 + }, + "currentMileage": 1358, + "requiredServices": [ + { + "dateTime": "2025-10-01T00:00:00.000Z", + "type": "VEHICLE_TUV", + "status": "OK", + "description": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin." + }, + { + "dateTime": "2024-09-01T00:00:00.000Z", + "mileage": 30000, + "type": "OIL", + "status": "OK", + "description": "Nächster Service nach der angegebenen Fahrstrecke oder zum angegebenen Termin." + }, + { + "dateTime": "2026-09-01T00:00:00.000Z", + "mileage": 60000, + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Nächste Sichtprüfung zum angegebenen Termin oder nach der ggf. angegebenen Fahrstrecke." + }, + { + "dateTime": "2025-09-01T00:00:00.000Z", + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Nächster Wechsel spätestens zum angegebenen Termin." + } + ], + "checkControlMessages": [ + { + "type": "TIRE_PRESSURE", + "severity": "LOW", + "id": 955, + "description": "Tire pressure notification: You can continue driving. Check tire pressure when the tires are cold and adjust if necessary. Perform reset after adjustment. See Owner's Manual for further information.", + "name": "Tire pressure notification" + }, + { + "type": "ENGINE_OIL", + "severity": "LOW" + } + ], + "combustionFuelLevel": { + "remainingFuelLiters": 8, + "range": 123 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + }, + "capabilities": { + "a4aType": "BLUETOOTH", + "climateNow": true, + "isClimateTimerSupported": true, + "climateTimerTrigger": "START_TIMER", + "climateFunction": "VENTILATION", + "horn": true, + "isBmwChargingSupported": false, + "isCarSharingSupported": true, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": false, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": false, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": false, + "isEasyChargeEnabled": false, + "isMiniChargingSupported": false, + "isEvGoChargingSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteEngineStartSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "sendPoi": true, + "speechThirdPartyAlexa": true, + "speechThirdPartyAlexaSDK": false, + "unlock": true, + "vehicleFinder": true, + "vehicleStateSource": "LAST_STATE_CALL", + "isRemoteHistorySupported": true, + "isWifiHotspotServiceSupported": false, + "isNonLscFeatureEnabled": false, + "isSustainabilitySupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "checkSustainabilityDPP": false, + "specialThemeSupport": [], + "isRemoteParkingSupported": false, + "remoteChargingCommands": null, + "isClimateTimerWeeklyActive": false, + "digitalKey": { + "bookedServicePackage": "NONE", + "state": "NOT_AVAILABLE" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/charging_sessions.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/charging_sessions.json new file mode 100644 index 0000000000000..47c9cbddb05ae --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/charging_sessions.json @@ -0,0 +1,14 @@ +{ + "chargingSessions": { + "total": "0 kWh", + "numberOfSessions": "0", + "emptyStateDescription": "Charge your BMW and track your charging history.\nSession data collected from 7/27/2020 on, your activation date.", + "chargingListState": "CHARGE_AND_TRACK", + "costsGroupedByCurrency": [] + }, + "datePicker": { + "startDate": "2020-07-27T00:00:00Z", + "selectedDate": "2023-01-21T18:57:42Z", + "endDate": "2023-01-21T18:57:43Z" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/charging_statistics.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/charging_statistics.json new file mode 100644 index 0000000000000..321efde21e348 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/charging_statistics.json @@ -0,0 +1,8 @@ +{ + "description": "Charge your BMW and track your charging history. Session data collected from 1/1/0001 on, your activation date.", + "optStateType": "OPT_IN_WITHOUT_SESSIONS", + "statistics": { + "totalEnergyCharged": 0, + "numberOfChargingSessions": 0 + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/vehicles_base.json new file mode 100644 index 0000000000000..a474f82599b7b --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/vehicles_base.json @@ -0,0 +1,49 @@ +[ + { + "vin": "anonymousICE2", + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "mappingStatus": "CONFIRMED", + "isPrimaryUser": true + }, + "appVehicleType": "CONNECTED", + "attributes": { + "lastFetched": "2023-01-21T18:57:39.114Z", + "model": "X3 xDrive20d", + "year": 2018, + "color": 4284900966, + "brand": "BMW", + "driveTrain": "COMBUSTION", + "headUnitType": "NBT_EVO", + "headUnitRaw": "NBTEVO", + "hmiVersion": "ID5", + "softwareVersionCurrent": { + "puStep": { + "month": 3, + "year": 22 + }, + "iStep": 553, + "seriesCluster": "S15A" + }, + "softwareVersionExFactory": { + "puStep": { + "month": 3, + "year": 18 + }, + "iStep": 531, + "seriesCluster": "S15A" + }, + "telematicsUnit": "ATM1", + "bodyType": "G01", + "countryOfOrigin": "AT", + "a4aType": "BLUETOOTH", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + } + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/vehicles_state.json new file mode 100644 index 0000000000000..037a71144c823 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE2/vehicles_state.json @@ -0,0 +1,155 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2023-01-21T18:57:40.809Z", + "lastUpdatedAt": "2022-12-21T12:40:32Z", + "isLscSupported": true, + "range": 497, + "doorsState": { + "combinedSecurityState": "SECURED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED" + }, + "location": { + "coordinates": { + "latitude": 1.1, + "longitude": 2.2 + }, + "address": { + "formatted": "anonymousAddress" + }, + "heading": -1 + }, + "currentMileage": 122912, + "requiredServices": [ + { + "dateTime": "2024-06-01T00:00:00.000Z", + "mileage": 29000, + "type": "OIL", + "status": "OK", + "description": "Next service due after the specified distance or date." + }, + { + "dateTime": "2026-06-01T00:00:00.000Z", + "mileage": 60000, + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Next visual inspection due by specified date or, if shown, when stated distance has been reached." + }, + { + "dateTime": "2023-08-01T00:00:00.000Z", + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Next service due by the specified date." + } + ], + "checkControlMessages": [ + { + "type": "ENGINE_OIL", + "severity": "LOW" + } + ], + "combustionFuelLevel": { + "remainingFuelLiters": 35, + "range": 497 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + }, + "capabilities": { + "a4aType": "BLUETOOTH", + "climateNow": true, + "isClimateTimerSupported": true, + "climateTimerTrigger": "DEPARTURE_TIMER", + "climateFunction": "PARK_HEATING", + "horn": true, + "isBmwChargingSupported": false, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": false, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": false, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": false, + "isEasyChargeEnabled": false, + "isMiniChargingSupported": false, + "isEvGoChargingSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteEngineStartSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "sendPoi": true, + "unlock": true, + "vehicleFinder": true, + "vehicleStateSource": "LAST_STATE_CALL", + "isRemoteHistorySupported": true, + "isWifiHotspotServiceSupported": true, + "isNonLscFeatureEnabled": false, + "isSustainabilitySupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "checkSustainabilityDPP": false, + "specialThemeSupport": [], + "isRemoteParkingSupported": false, + "remoteChargingCommands": {}, + "isClimateTimerWeeklyActive": false, + "digitalKey": { + "bookedServicePackage": "NONE", + "state": "NOT_AVAILABLE" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/charging_sessions.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/charging_sessions.json new file mode 100644 index 0000000000000..6c43b37be62fc --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/charging_sessions.json @@ -0,0 +1,14 @@ +{ + "chargingSessions": { + "total": "0 kWh", + "numberOfSessions": "0", + "emptyStateDescription": "Laden Sie Ihren BMW und verfolgen Sie Ihre Ladehistorie.\nLadevorgänge werden ab dem Aktivierungszeitpunkt gespeichert, d. h. ab dem 27.07.2020.", + "chargingListState": "CHARGE_AND_TRACK", + "costsGroupedByCurrency": [] + }, + "datePicker": { + "startDate": "2020-07-27T00:00:00Z", + "selectedDate": "2023-01-20T10:38:08Z", + "endDate": "2023-01-20T10:38:09Z" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/charging_statistics.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/charging_statistics.json new file mode 100644 index 0000000000000..c3a23ad76a894 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/charging_statistics.json @@ -0,0 +1,8 @@ +{ + "description": "Laden Sie Ihren BMW und verfolgen Sie Ihre Ladehistorie. Ladevorgänge werden ab dem Aktivierungszeitpunkt gespeichert, d. h. ab dem 01.01.0001.", + "optStateType": "OPT_IN_WITHOUT_SESSIONS", + "statistics": { + "totalEnergyCharged": 0, + "numberOfChargingSessions": 0 + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/vehicles_base.json new file mode 100644 index 0000000000000..2981a866abedd --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/vehicles_base.json @@ -0,0 +1,47 @@ +[ + { + "vin": "anonymousICE3", + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "mappingStatus": "CONFIRMED", + "isPrimaryUser": true + }, + "appVehicleType": "CONNECTED", + "attributes": { + "lastFetched": "2023-01-20T10:38:06.689Z", + "model": "530d xDrive", + "year": 2015, + "color": 4283055410, + "brand": "BMW", + "driveTrain": "COMBUSTION", + "headUnitType": "NBT", + "headUnitRaw": "NBT", + "hmiVersion": "ID4", + "softwareVersionCurrent": { + "puStep": { + "month": 7, + "year": 15 + }, + "iStep": 504, + "seriesCluster": "F010" + }, + "softwareVersionExFactory": { + "puStep": { + "month": 7, + "year": 15 + }, + "iStep": 504, + "seriesCluster": "F010" + }, + "bodyType": "F11", + "countryOfOrigin": "DE", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + } + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/vehicles_state.json new file mode 100644 index 0000000000000..da0d972911051 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE3/vehicles_state.json @@ -0,0 +1,103 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2023-01-20T10:38:07.492Z", + "lastUpdatedAt": "2022-08-29T16:04:30Z", + "isLscSupported": false, + "requiredServices": [], + "checkControlMessages": [ + { + "type": "ENGINE_OIL", + "severity": "LOW" + } + ], + "combustionFuelLevel": { + "remainingFuelLiters": 43 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + }, + "capabilities": { + "a4aType": "USB_ONLY", + "climateNow": true, + "isClimateTimerSupported": true, + "climateTimerTrigger": "START_TIMER", + "climateFunction": "VENTILATION", + "horn": true, + "isBmwChargingSupported": false, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": false, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": false, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": false, + "isEasyChargeEnabled": false, + "isMiniChargingSupported": false, + "isEvGoChargingSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteEngineStartSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "lights": true, + "lock": true, + "sendPoi": true, + "unlock": true, + "vehicleFinder": false, + "vehicleStateSource": "A4A", + "isRemoteHistorySupported": true, + "isWifiHotspotServiceSupported": false, + "isNonLscFeatureEnabled": true, + "isSustainabilitySupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "checkSustainabilityDPP": false, + "specialThemeSupport": [], + "isRemoteParkingSupported": false, + "remoteChargingCommands": {}, + "isClimateTimerWeeklyActive": false, + "digitalKey": { + "bookedServicePackage": "NONE", + "state": "NOT_AVAILABLE" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/charging_sessions.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/charging_sessions.json new file mode 100644 index 0000000000000..6c43b37be62fc --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/charging_sessions.json @@ -0,0 +1,14 @@ +{ + "chargingSessions": { + "total": "0 kWh", + "numberOfSessions": "0", + "emptyStateDescription": "Laden Sie Ihren BMW und verfolgen Sie Ihre Ladehistorie.\nLadevorgänge werden ab dem Aktivierungszeitpunkt gespeichert, d. h. ab dem 27.07.2020.", + "chargingListState": "CHARGE_AND_TRACK", + "costsGroupedByCurrency": [] + }, + "datePicker": { + "startDate": "2020-07-27T00:00:00Z", + "selectedDate": "2023-01-20T10:38:08Z", + "endDate": "2023-01-20T10:38:09Z" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/charging_statistics.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/charging_statistics.json new file mode 100644 index 0000000000000..c3a23ad76a894 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/charging_statistics.json @@ -0,0 +1,8 @@ +{ + "description": "Laden Sie Ihren BMW und verfolgen Sie Ihre Ladehistorie. Ladevorgänge werden ab dem Aktivierungszeitpunkt gespeichert, d. h. ab dem 01.01.0001.", + "optStateType": "OPT_IN_WITHOUT_SESSIONS", + "statistics": { + "totalEnergyCharged": 0, + "numberOfChargingSessions": 0 + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/vehicles_base.json new file mode 100644 index 0000000000000..b13ec7a28db9a --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/vehicles_base.json @@ -0,0 +1,48 @@ +[ + { + "vin": "anonymousICE4", + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "mappingStatus": "CONFIRMED", + "isPrimaryUser": true + }, + "appVehicleType": "CONNECTED", + "attributes": { + "lastFetched": "2023-01-16T15:33:53.940Z", + "model": "435i", + "year": 2014, + "color": 4284572001, + "brand": "BMW", + "driveTrain": "COMBUSTION", + "headUnitType": "NBT", + "headUnitRaw": "NBT", + "hmiVersion": "ID4", + "softwareVersionCurrent": { + "puStep": { + "month": 7, + "year": 19 + }, + "iStep": 539, + "seriesCluster": "F020" + }, + "softwareVersionExFactory": { + "puStep": { + "month": 3, + "year": 14 + }, + "iStep": 502, + "seriesCluster": "F020" + }, + "bodyType": "F33", + "countryOfOrigin": "DE", + "a4aType": "USB_ONLY", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + } + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/vehicles_state.json new file mode 100644 index 0000000000000..93ebca1d305f9 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/ICE4/vehicles_state.json @@ -0,0 +1,103 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2023-01-16T15:33:57.194Z", + "lastUpdatedAt": "2022-06-14T08:21:50Z", + "isLscSupported": false, + "requiredServices": [], + "checkControlMessages": [ + { + "type": "ENGINE_OIL", + "severity": "LOW" + } + ], + "combustionFuelLevel": { + "remainingFuelLiters": 20 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + }, + "capabilities": { + "a4aType": "USB_ONLY", + "climateNow": true, + "isClimateTimerSupported": true, + "climateTimerTrigger": "START_TIMER", + "climateFunction": "VENTILATION", + "horn": true, + "isBmwChargingSupported": false, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": false, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": false, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": false, + "isEasyChargeEnabled": false, + "isMiniChargingSupported": false, + "isEvGoChargingSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteEngineStartSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "lights": true, + "lock": true, + "sendPoi": true, + "unlock": true, + "vehicleFinder": false, + "vehicleStateSource": "A4A", + "isRemoteHistorySupported": true, + "isWifiHotspotServiceSupported": false, + "isNonLscFeatureEnabled": true, + "isSustainabilitySupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "checkSustainabilityDPP": false, + "specialThemeSupport": [], + "isRemoteParkingSupported": false, + "remoteChargingCommands": null, + "isClimateTimerWeeklyActive": false, + "digitalKey": { + "bookedServicePackage": "NONE", + "state": "NOT_AVAILABLE" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_frontView.png b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_frontView.png new file mode 100644 index 0000000000000000000000000000000000000000..bb2f96f7f2261cf2a9f9830103628f643bd86877 GIT binary patch literal 118802 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

9+m9l7#C=0E!P!S_|C`Q3N!}jDx*J` zAyTzqpf^*Mp=AjCf`=A_WLvZvS#gJ<3%gYfPcVqR$(yw8v0=1dbv4T%T?)1q%zwdQ zOR-H5GbyJ11XeRM15H*7qgP(q-|p{3VPTl^6*8eQ+yUdS9zuq9I{epjqv3VoNv20d zEI0F-!`4jhYV2q$KBKbalY=7!UE;ocjIZL!__$QWJGY=9kD#FRAq~j!5Di8^1(9de*b^Ym;Y($FaxCia zgm4+nCU`5KH_+MyN!I3QCW+q4$89WGN^wtAaCF1ymZgoQJ048EnoRcYB4KmXk3QEk zC}zpPV{hK7jKgMfn^G+1c*6Fq8z-f*sTdo(iD$Oj={Y7zvI!MIhP2mHXDUIFmvg+O zv$Nw#vx6D;WV|IVr&sclQcz}rX>UT7iJgboWmA&^iPENf35iV{!U`&;ZBs=pO$t`N z+sh9R({Idg_+#kJ5I7vduwVnW^aK|ml!Y!WZ`C_YE{Y%4c1-*L`U2)&S2b z-J3I6N21;m2)pvM!xzp(&9a_t91U}f?JW7Vb9LbeaV352m-m{RNW1sO?Z@8NRs`p>KOECo63?Dlr6vT;z2nptWuzqll+WZ z40Dx7)1CfHSg90bGeM6yrD!&P2|0+>?5T6pQ^=Kfw}tHQm?OU~JSzIib89NIt4=ht zqT(PAimrfl%IqfTvIqxWNM>S%2;?Kx;mG!{G3;O;_1Vr)#1oqfEKFipC<5Gr+!POH z6Pc>^9gEpyem+x-wOh7M!mRO>f(Ag^K*DytV_xE{n92d=SC6LON(4NZhaP1WYzOvg zwAsFaOdk>jyX5tzv(scVH*Pa&`F7M&z$WP&S7CQz6GVHu9Y&u4QHBx7^v}7!(|0kF zckw9pNwX0LdJe&!erV@|8u-7tP^3O6MK|GgZrag|kc@->rgQtZZ9#hE_>{MevrP-L z8>oIFOhIVphjws2etH|^DE2|b)Pw1-lk?(28>D(V(5WLZ@*dU*vR^3&yJ48cW2DhA z1fcg@&G71Xm=~uB6fAOPZ2?lH6(gP^$BsiQkwk35hA;HuGcLe93hee_x@{*MOIk#n z9Yz=4;^Ha}6Ovg=N{E{{Ex9~e)WmacB+*@NmtFnwDKIa>-q<7)d+A2>=~EH)*?(S_ zMSLR^K42ri8Hrk^4y!K)X)nbzrf`~G=i)ZSa8?}7G5W|O7MOCw=E3ThLv=c$)rsI3 ziEmgB^?Gh~C(Ezeo`hoJ=~}?!s9)P*vd|!8!H&sqW(+k*+?9v)5~81Z$k7wv25AEJ zu1C(!C&_mRA1Q%NFUicr4OmsdXE7c=-7xGZI*g zmA<+OnM4?1LM5*U)wh?cWs6J3RwvGKrm|sDnlNrSJxpa@~eJmp`QU0G`^ux zF%b5)kin+HKWHGhm|w7DY8E`lruHl6lO+?Hq7_x>9NCLsWI6iH^RR7cT7a}?gy_}; zNpUr#1CruJGz@e1%6xhEj@Tj zIYJ~+fp+O8Z*+xviy5)XD%wq$&!tK2R=G#8vD?(jKp|jbMbz)c@NNJl!u^VhALN_; zzx&^M=Cn`oZSf|ivO#5nFt(z_B{PoQO`Ld?Ev1Q9$K|5zM?sN;=YI7I`#<;)fkuzd zc}W$T=YEuPUgU;7U?j_@qt=$tIWEwk!w{2J=z7nF?#@J*7$AR4e&!C88OU+K*1onDV7o0C|Ey998+;^!ZRN#CCXfm&TN7&TR#;Aps;pK&PZLOb}x6iOb#8J*$HZ}@A#^<-w zb6T!RR#8n}>6$e&tJHRr?DlzQNxFBps!Uv>Bo8%KKVJ&mV>za5KTKEazcBYnf#+%< zFDt2Od<;e=OjIUg_n1g2Zgt7#l!!U37Gej{z>x+RxxYLEJvL{7h35~TXb{6=<9}Q* z-`k@)+zpLv#tge$i##*m;lvkOsyk{Pl!3 z4H|!x7skS@2q^gT&I`Seh{Y;Geq4$KE|qvx7Tpc_XqX7>%tCyL)vEp~Nsxd-gaO4@E#SzlFOKNsQr_0Q`q9)|;V99;VB z7bd*C2-OzX-}|>uXw9ov5AGgSJLO17F3p%djoNr$*@MWu_lHv z7<&*XGT>ixzrTp~0`Y0Jl+!euNA_|h_HxImos*yv9h!YP9(mrSUU}Z;`}&k# z?tk7hYo^Nh;_yp-_~N}Y?$Ln@}%7#kI(2t3`U}nX4sN4*P5l$m9>vljB;sVU8ZCFGJ>iOi+g(#-J zlk5<>Aop*hDVtF5_owQefOoDd`fmoyCg#)c80R_X}q#07lusma3SFtERqd<&im)_`@I)jSHtag6dP8bljj*+Vme_z4Y2Nt}nTKBP4zeOo=-I+CLz7lkDM#&!9S>u8 z{Zxcevm3PiwnL{@6Hpk?sYZNs+g+OB8~fYn$zk(A@nGMOSp#;t^nirt2BAQQH^?9{#g zwrl>p_QHA1#ZT!-G+nMsp<|wYpw?R0|LKvN7k7Mw7LwtH4;um>V1XEl;yHwhAhr zh)M)IW0Uz-h+7^#igLr<=gLm6LM&OipB+=avHt``ceD&1@|$mbe-ZV$@9k z;9a5Vpk~mL;8`V;!&L{{bFkDEZ+5R>VZ_DpEsJJp@P4 zs3rcNZuW1(ouC4cBNat*fB+LnLT0?D9esNKmAzdiRDe*GGd*4HHZwlWEgeLoxAuJsL#;u28Jg~2!}#huXy!d%stBm!MHp_um~bItWP^med)ADQ46q1KS^NH! zBj&$l{)-r5TrdE7*YHB}@KbOAfY(Av`nw6Oc1ZCl?g*dJHP1cp1gv7nnp(>6{3x7w z)3|Ed_{(%cU6-)?ekOVjU&8)vQIzMOw;d;hTaT{2Ss*|vm6G*VmZsv1RE=io0H6

Ak zh7ZC+sd2DejH7$dX<0B;AbhYULij9ih*tv0T4DMS{~P>?)ZG{&QcQDzFm=rHM{4+?b{Ma;$kluT-aBRtl$mE z^gyo1#8rp`y57+zBvI^wWH`6Xr0~|QO1unZ=9Eb@t>`rFv(+Q)Y1v&ky+Tr)HRb>r zzQI}86^mSrBb!`hV|@Ey8+p{qA9@7eg-+y(;2&Dzjum6kUGL#)mvnt?@7_3ECBL3# zlj(W0zq3+azN`7)Cx(=*MD|t6Grq$9`~C<5H|UodS)9oCMmos=L0Ug(o8zO?i3kG! zgHm@9BY4=)pA$e|6nKiTu4EK5W=J*?4;WYEA2~fAc?HqD)95WBKCv;RFKnmKM^QE-11nvrC{5bX1 zaj-U74|^db1-2?HbbRk$I?%4x7j(zxPgKm_Q>=#mFEsnI?o4uxT!{|2HWAtt6Povs z(ZiGoJDnt9HrlF|V)pW>S_-i}?OU+NTmP(T2c#aR)uWt)hUYc5wok951T1MZwy3Ry zQh&z>im>h%h1!y-c?7>9pCLryWw$vmbbPDUV#g7XiW1sS6dHuXi>Qtb;aQ=1z~kiC z%NiLOW#CbsT}1uB0k_)sP|1a051qy4gG$v<+Y>s42Zu~H48H#Wl9Qtqab3x~7scdM zvNy=e$|1?12w2(3f;b?|xyKL+oW;ZI*g656MQE{A>D5j#vNfg@%O!gpV*@V(UH*`2 zay8CelIu9epPh^?s>>lgE5^bPLC=ee-X(!pl1?5(A`Tqw5!f9dXMcg{g9SLTTlUO= zdj>sNeJ4LOvoqiZg`v=SF6bCFxG^xF@Tzk8^=N()fMbdT`~(ESt4i0iaX38=BujxO z=fIguC~~vhS+)$0z$3&{!eO^Rz(|PNhj75*0y;Z#9Xik`Y64m&8-@=?tuC)`Qb=)U zHfMfmq`f#WeV4ZSK5EJD3J1NWzq4v$?}HM6u5LtNN~XS;Q5E`>W*xJ>8jNQ(g3-MQ z6v!8_=1lYc!&hS(h1!3-K+WLC!A(2|0hmg8 z)Vf&*EwdBA7eO&hWa_^81wd7`QlDlE2$za5EEH@j9N2X=4x%x8ojy$#=8r=y|0rk; zxf7CFUf&znBELTv<%K5R)J`&17wy<1hB-h$jOPZ3<6%9HvEnUuyljH8^1sk2%OzVT zMgDs86oy=4X;+-14@O7A%HIZ)aunB$Bb~&M(-|9GY9PsVk#zEe-Daf3pG{l$J7nKbjP?=i6AVoZ{Sdz;1P%pm8mRJCKMh+7;s8bn%&)N?l?>W*dJ?m=k_VUx zhg^oB3<8WKQ;S;FeEC^f$ z@mP476EBp}VBc|)vG+i!ir@K@PUe^(jml1oa}eXo?F_^nlLWJVcJZgDJ)x+GrzQH* zC4}NHis2eKH5?|z^e=cbbW9SgxIIC>3%Cr}cmEoM6)>Uis;f{D?I#SfnZ2qF;|#Ut z2l^uCenB&_1`~_0%Qf`OeM;>hqr0C)?`2s#oj2jkq}8#3p!-P96m611>BBIt4}oh8dVOz3}9M2 zl{XPDy#Lht?%8FE&vmW+eY4uk4H4JxOy^B3<~Ff59dBYIM)M1)0b0BV<8v5z`>bZr zJI%g#325$r9n?6@LRbz!9gPbj0o8-sN0u4rAc9Y3rlrV$m;SUi-1du=`Uo_>W7!=>n0Beh;wyS zmJfcjfZ44`#?h*f5#b&4!hY~Ym;=bKX3PLPBzT}Re>LVa0cg`*U|D zNbJ3`B|4}{-G#}VwJ=5_x;8p2x{Ucda|6eN^>8FL#~x~YVN?TR27u2{U=Rn-D-C?E5^otwDRN9il&pUnm}yWNa|*bp zr)6s{b;{|&^OqWo1%uKs@cE#XzcLvsQg})dEOm-0$P6;c3{S%L)~R}=da#(Xpe&>~ z@+H80$XX!|02?5`9zYnpWen-$>C>%YL$f=;9YDijn?Ok#Si;#)G_hE@Y=rG+y&AJ6 zdb>q5KMcuc-^1z*57)=q$Gjzaw&`ljUe+x!WlVj{dzmyu-?{zl+{7o{#KZ&t=`hPG zFtMT*Qs%;(SO2eYYinCkNC-mO&Jdk8>K-Bu9Vj90Q60i+KSr&2PbroKCl=wvH@oY2o~zyx1~#|O0h7mom-gzP=U@KZL(a*3(_C6!1!BX z;7Lj)!A6#dan)S!l-tcEM<&37$*StX?-|Z4cr+-H0XhT`K(uEJ(+E0LL+bqf&u9M6 zF9xN8cp*{c9$Y^3d3^udHTGpZ;hXA?h+oey*Ku?ZUcpqXGX;^_Gx^2S9Okpu48Hsb z0yl&W31*9K<+D`7l_l?7i5EQB5SNz?P#n+&rdC@4tY!}Xr=P$&i^ zM|ME=au3KgK*#-l6wyGAvK7O31V;}#CK?i5?mnr_-HVaSY7Zbo7 z=@?P-hg0E)IQbAlOcY801A=w|5j(iYW>dEoM-_ivP)SU)L$>e!X9 zi65pm>|BUEgP*hlQh>L#gq;PS8Ct^ZHXlGMXFdig0<Yck-UA#+j!PEY4orzNx_Hn3sffdLp4!{Pv(<}!F$B}ncw@b2NB!9}tFOT#c$ z>KP2SgHRnzKPVMQJry1eSz=f6Cpp~e1KON$Q(197npn~7=l@9=hF;`{>#ywjz3x?% z^Fch2N)bE+`oOP3UK{KxfI7nT+)k4*d&HO+8jQV3pgxDUM3FwUkR>1oNQ9JSRZWnY zkm4!}8!H0;KD`L}cDQC5(yG_sT7w-mKsa##)HcM736wXQGdCn4QR)N| z&k*|mEBa&{%iyuMZh?(g10V+!F8LeBLd#+SE4BxI0NU~%k!rPqQ&mU+yKumZ3V+sE z0#xHdD2xyW=_I(Cju?pS;}C$LM>r(A3;yLTewT!WXJvR9SL3Yz#^*qq8KG?8V(^5` zvRuarRH@Ky3(l|BxE8t(Z9FL8fAo|voI?GMDO_SEpLN*TWYL~h1X@WsY6<5~zxRiv z0uVM7F9YHVeGnDH)4+D&#h`YhQ);x zBZ7cDJ^(~H8LqP0ec<>ZCW;GD|7$nQNo;62A;Ii)7aj^}JW@zD5ECF|$e)DR0f^+N z8mI%!2KYi(B}iT(?mK}Rv2*0I7TdC7jo8D(2YTGMWsQ&xnE7g+ompXlgH~^Ub zx}X0=CQswk62a|7H_1Q;pgs>VWH})INF2a%4HU729|)7GFt!Y z(nUxmSH1K4r1A@){oU0qEegx`&ux?5t7qT|na}JE5OI5rB3gelABT_)cmz5I_ytx4 zJ|GB;c2xr4Lg}xQgY2s+3hmm+0!=~p-M9!Mi=-Jy1>hb655#D}g#q|Z2e`Bc6oi{c z_=J2T3($L4A@Bo_ssrs~$ixd;|9`3ouL$LzdD8z@+lLyV)=>!U5@~IIZl6?U(V$7y;sxsjuLXd)2*Pv`qy&7{LPk^@B*mNpGIkS$ z=P%Ec1!e=I2SHK*N$lmiDi?04lJSx9I6(8WO)uX`eNy=et1tNbJtbq!OQgN=>L&=n zgRH>Xy9)iI91MJ={8K(XaEOshd|I~bK0`*~>WEK3n0zp+bNIhF=W!I_gyp4)bM1n- zcmFoDS%^p1;r}Wt1MM+Zj0k=Yz~<4uGUGII0^FxBuVhSQAQ}Kh8aD1TX$D_(&qM|SHH;7mG90i*ws0Q^WduP1HW>8C9K;5wzX^pv zy$P#;oNg2n1gQzllcCg|5xNjnH$1Jqae2KzD{D<63ZE33%4pvoAg&I63)U#kIT)6S zWdv@~O#>!C?h7D)4;LV}m>^iNWDAG`A!CC+AYKR({(_9RuzW}cRr=o#r}>|FOZ5V{ zgMS}&IaXGX3qVLmISQc=$-nRL?=y%KpcyigC?j%6CVmuM7y-CBEIEVE!;YiWWlVc; z?fb0!g~a@Y^C)w_C6v739S0iMTSdv3Km78SGdK(59}w2w02HDsn}axjJYwNWwkQ@3 ziwL(3rl_;)umPAGYYfU1KG^#2PRf?DjSLtcV>XtPD9&;ksieUe8QllsZKP)#TAEN& z$HG!Q@lq$w%Boa<>;cFN@<&{&22wvArqZHT(ER48pEsc$WnbB?X1^P>x!<^#OxZ!KY#w*P70rg=KRP@w$GbNrr@l!g?dUxzSfy9ucE@+x1SaHaSJf; zA|e^+{odR`A$GsszLWMH4G!DZuV!%RuPSul~|K5+L8Z-oNxu)fB*{ z7=lDEBz4EeZrnPx8_3@cq~B=atROrl2lfaS>ov?xA5DO$61?wN>Luiwm>WP?QjUBG z)&FlAu@1CLLuMFfzd_1)CkHx69!i+3q7>_b8a@&Ftq+09eAz%s={WFSn z|As|==!AGL@-9IG%*Sj%d@J$l`*~Oa6iXy^5u)L&NHdRYFPbp4IrrZ`prR+?ZVw=d zhiye+ai~f_g!~>#02l#6qMb#r9tAnSbqd0LRtXCyc-ibmi|mUFz(?TLO;*f)4zefP+*I8AfvigD=kr zjiH}Y5cV=O3K?!dh6^!QLAt$QKSY3()C_(rqG_q|vVGx^t;eI_v!2E6#O;G3Z5*rFoHHO?*7H#uBzeXw)kG8}o8zx2|1`H3^J zPVbZ8FSG2_8LD8P#y!CF?A@k3y@1i;Vpnw(9}Z=_Z&Q?zsj(xJvX9k zSUIaOy}TCMSA4goq@=ba?A-N(pWdm7kPclckj}m(V|jmTYi07iY5F^Xz2&t7AzN+= z?%ixWa@=Lh%+(b>`HxJQTZXVYl}lpLNjeF8O`ledgh*bxBYoXN-^BNneapl97kaYt z^1gik^5v-YL|tERn$?FB%}a+#J;MQdn1HK(rC!2=uZS>EGmh`+Y!0L(aQykqjKS8#(H3o?p4?FVp|0861;O^Z$?_gqg;#teyun{;4@MZPdg>O+OmL#enHN7pFqsJ@#Iu_Od zX9j;ZycZrD(p!fxo^~YsM_;&!z}-0d9jQds7L|YuKL}4ws}9n~eMup^tJ7MA|K2=W zz%~`-L3o4EymZg4W$+EIZjh)1{&roaW}nagZ3P*|u$F`K#($FKyS{UZLE4vlTKvbJ zbFg~nzuhTl>%UQ zM|)C;WOb-AV@=@G?!F*B&bsMb*fqy46;jA>R4@9g>j1lnU`sxd;f>Jl5+%3>;RUn| z4BRvjA4@C1b-4I9>bZb~3M7y?CiQ93J$f zR&?|@Iw~%+;0nu{(o7?Z5thC5X=M^0i=v^Iw>Nae_@V`lJO+=T)1y)i8NE^|*v}!} z!MZ#?mgyzlK?oUdf=f$L{>dP$YSDm~9%g#UsTwC^nGV)M5lBJZ_P_hi*nD_-oKDpM zHm*rnGI1wcbumbia`s{3QES2K;z^BdBrPU+<40)JRPGv#JhT46+ZD1UM>DjNS(9nm zp7X=iXlaH~gFe0EFZg3;}L3cRCW?G)AObZTz zaNoW3;Uub!gCe=9@89={@NVV3C}5y#lE?)$Z13Um{G6Zz2(7O1$91~a)&&^=zF5n2 z{lgd#2)pVm=;4qex@W_Nqe(m`k0O03_VSj!KfylDjKRrTeXAW&HqbX z_qpXo_bcegn5u^lx&8{Z7#sG-d1DM*2fYg*1n%FLhfUydK^#+Cp&%YzPstS?5cU#x zYqOqc*FL080c7kDq55Ga*0;K&GD)*frVI1;Z(gOIcIW&cL0Tp>&JhZ=e-|I|`@zdU zok}K?nPBwx_w{1S2WF0v_LV|)ps!t~;=zMGuU-krNa9VPjF^HJA-s`3p6en!Sckzm znqojlr5Ir1TU|T`V1@-%K#CoZ=@&_-WC$f2qD=qMy>&Wg{Zqyy+C!%t1p91ff4#mn z*tswqDS$W0@I*7TCx4%t;^lz7t$IEJeR^hx55tCO-@_`x`_&18H2dAXi>{AYtC{gF zz=uwiHH^dC5EkF7=$Jcy_w7LbSy$zlf>np}qDer#n-1XvO83fSqr?9|bJu1^!99)jE z#bt(=vUZh#I6pm3DCBYv0|wpeyjA=Pdr|uQ+-mAbU|aGuXH?Shrn$nhB?h`!jur zGs@^iBuw|y17T89RyHVkeJc~+tg;EI0&uc(i>~o?@TIEy*>we4`RQQ%ME73g8^rIqXl(iHNzd8aBA6+lQ69fqv!HbHEK3h^*( zkukd6;(#S*rZr5+cQ|-eB|x{$;TMd6mq6 z13hb;pA@JAsCpzF%tv)-8QNVMKzmpgCxx*lb3oLN{WmI=}HxEE> zrTo@p!{x!X`M};NLhk$IAY%Txq_X897}PPmc2H|=S5&hEbd}DBFSK;`^;HE7Yaz4d zSzQdaPZ<%H*RVgzxK{{iLGR7bZY^}dCr&X1u(=40;9)1!6%BuC8i9>_g;kExh_Q3| zmJVmr$3vs-uu1^F*Ws+*i?R!Rt2UgjQXtjWd!!T%;S+Ec;39~>fPBIfPHf~eAOLV7 z%j(b?9Fo}uP&0<%-A(!&T#E|(u0%|^-zzH%g2EOXTieB@sI}=Ja7#{hC2l(H?X6zl zf4lL$yZfl-*Ah81R5p!XFaLe8{qgyA9hE$Tbvtm&)?$f#ESnG|A|Oct1@L7csoxMU zZdtS(NDn+dJT-%A00zAF1$o_ir5(;6g`KUT0Bq zyF1W^=`DYCp=bUOXN-Aa3rrz-xccX-oxQ!Af8-8gbxn=>**SSQ@!Eaq#bshZ9p~*> zcooXpqI1Vl)#{P8Uq^zVp9^94Mi%Hg0mDkUdzU^iAcl;CJ8TxjEQjXDt*i|=nZb7g zvXF_Df@{lnfqZ+|8G?avRAd)ny$(kKKkGOW@#ScC9oNUhfBw8_9Ab*3JwLHLP4cQj zGWu-t^wU`p5Db{mxs`_lHHE>veMKcPwe-Pdo^NOC0B^j9WkE0<&{R5^zq$itR zJ#pl)r7-VBk5F^Ym`Zt>cK1i`_gv)N{&D6565sR*b?t6shw6^QbSE@H`eRcJtNh-* z5{V_CI7nTb5;3odGr_^Zz>_(}o)J|h8H~^^34kUuGn3vJrTy~d z%l+Ahv5KJhE0OunPw1N@-o5)rh7y&9|8v#TPV4MM?pdiJrXNz!Os7BMPFlSp-=%eJ zLY-=Jd2-SRR^3fa1i5smSs2Z=sH&{gD|>SrhVfQ_gNv5bI8WfKO*?Sb{dr%*{G9e^~5nE|`< z^|ae3#B=TXbr9!O*~*@5VAV)CC?6R#ewD zK0Xf0iuMpyf1#s#Z%G%8g_`G0oIbGK|KZr?@AdQlhp8_Qq;mbjJ#`w?sSKrPG!{}} zi$s(p6eaCSDMhyGNQSbhaEe0hG9=lVPK44RBpjqcR5mIVLdw{{E|e+5eb%Snz4wpP zQOW+k_g(K=&-1L|-9qBkPkqGUTd8<${ z=ak)WLbYV5Shd7QrM%077wVB~9^vrXsf(_7c(TMN6bZ;k8h8rNU^Q***0buJ7cd%c zAitoe2Uh_9L5IfP-Vc4M7<~r7@#B{_bCU53e57+$$)iW3sU6G9i%(+++dW_gHu!u7 zlWte&xD<*70Z+iWv5p9LFtwZpUvT=dEVXbH+cO4*Ux@SJm*?qMe}6^5#fZ(WnorRR zBTx(_0PVyLW&Br);B^4uY4di-yt1tKAJ18)$ zCPo5c0rYX*G@cWcWhlX5eGc}JFc5>9Q&PanMjr0*#}Hs>mt}jG&DNNCPHIX3zJbkN z!)D_x*lfIoSx~qHn@u59S&`1+c99blf+&FD*_a3X{ge!n#4pTmjT0W$*3O1`<0$h& zjT2TyIM8{9KWTs^nd0%lKnNut^pUD+xrn!?P>%!I*49Q=PK9E)akBdT`xZ0~z#9VE z_^*s@1rr0Eu?CU=RyuWREIO*jrKB`aWg|`90aUhGRAiD>(_9!dRj>Dh-+-WeU=nbM ze2E6wfsX;!vcY+jOKUZ`Hk759%k#4jdNQqf+$r#}7@} zF);2nP(pB2sRSoIA*NE6`7Nx0Y=oP8iHCXD~3(AfTfXvuCYEZ zeXf{id~8yH{OQLhX@G_^jjR1%iH{ zSv3wE&S`iW9AJb4GJY_e^;IoEZkfO~WGA?QnXnPdivYK22%%?@u=Z*+aOPt)$ui)5)O%DBq4tzBJe8t=>~yaV^jI_vp3 zXR7NC0OYb*k6*o#!1-;0We7@3!5=^nnAq;Kv8v@K7RSMwTre#)8iMQl-)1d?$AU>R zXBo@M3$3oU#-mm{y>ikDQV(mzE?7k<;yaw2o!wBpy4WWB0Z<_Isy@7x_5S&x!MBIf z?%%htoc8)m{J)i7@pakr=M$;*k9rX>$V%TZw{E`w18}I!y1JdWm+kvCmCYVh|GsEf zfT!R6?#$G>*ZVYw6w141B_}1}zOYp|i!Z(*=M6lZ_r2-DhF4#IUV9=Hq@JFg z{j{M$Zr!?d19b(zxP6fC9z46SAjjq7XvD{t`wE4^#RyLEN)&fBXNFSTg+_y80d|9R zs{dB;tp|P&ENfBG)6Px}>Yt+pONw%Cz+fGONERB3@W4eT2`^Vd(^9w}x5>=u&>{D} z&iQml25!L!E4j2J?XIr@D}!Z|bvN$Glfhb-@8hPov`$zhbD~dcf>X$YmfT4qVA z0TPW!Sy{P=A^|K9p&j1<0t#sYc%!W*D;Pc_Ju9oYV5oBVGFWCtTG}e0hRUx%Som?| zTv~mHTKjNWA-nrq{f5}q*W>&~<>uz{iW`UaY4)!LKye2$xm+32HLlrfC{49w$`dK5 zn8q-(w49u0B$Rhh2@_R{$15RV6IxD8R0N{R$jHFk`qO7G2Wg5n@QvNhfHt!rfUc6T z?a39giG{n>{;mIJwXh^iul!k8G7Quul^Z3;P62fQa0M8Wah81@hev zh_D!|=HB~#54m?htZm!2?e_7B#k(7dyjJKiu5M`$11>)Y+zfxX&6X}Q-`;854dJJut1AfEWdc?yS-fQ%7TK`saa2OexAHvj7(~r*>h*|xW7zVmP(6N{ex~~EKLCC8E0lbsC38SR zChd+|`J+dA^|B9ug_+wXwX_y@^j?~0uQ}KK`v+MJmEug9v)qU$ZkjjMsmmFvALDmi zQ8K(3y98^^+qcQrga-u~ADdB*d7QW(*y5gw_x)0nnVE zU)tTRRZ~*~H=T~(>{i>&&~BVZxOPA&gkvYI>yyX*iGVL*kKb-wc;+N*6{njfaoGp2 zFusNFY4-rmCKf?-Z<3 zZZ(*~*|TRgcbMNXeYP^5_7d&EX}#G&7| z+#S=im(MSh96gI*baTmyqnDO+71|DrMRB#Fb6$rrO~wNxdIp~5F9ul-F*sgn8CN(P zGhJ3TJnk7#>UsYwLBP#ru&Rt&wEr)FP6fOMR!1xX+fkDe zGn64Opjc$WY>k?MK{=o#0yv`f%YB*vCI|zbdLxJtI0JF1YpOxVRC0*;8w>(ia=%xO zn*XW!dxOECCRRePyUM=ScX&`$RTV$KL?bCF3G#7Ditu@Qo7(rKyH=OY3UR3Zs8?>V z3^6X@<-aK^(4c_cAqKvNx1&jp7yxQe96Ya8+qX*_J=K`r{;j#Y=8z+od&#Gu++`V1 zxSFo+x~!VSm-{v%fkIT^2}Jv4v(ra#5KZ4XbO=!vT=>V)6>$16N|+#I9L5CUHF5G{ zTj3*PTMMPGUvw}fp!Te&XdD{3^-!kwdgPqK@4LY};;a`UPXFrp)(We{VPy0_A^`-k z-yn+=mGwyL*s){Ly4>>%;s*Y@ckgKJ@qzh1UmAVSefEf2CV9O&NoAdr-$Sh;$~&Z; zyMWPr#DB&}1R*k|e-O#YKb5?TIAB-;Y4${LvlfndkeE&UYI12L(#gt?w zjGA~t%xL3Eu&imLWdp@DPWmfc{pX~}2*wl{%j7v^ckkH~>-}qb_RqpwlRU;B|H*gs zM3o0d#x235wRtVL+I=s?V2EpyHQ;tITg{ni@GP)*YG1)-(KIE@F2F4a@P^xYp;Ged z#>-=-zzHHqu4hhwY>`Q9E11nDvk|5VDQJXbjzoEUH%8&w18gJq!(1V3jxeaJ;Z=2Y z4xrXxszBPH_xbrNXzj9Uj(423Z34@10FON;%n6{jP!a~<>Y;;CYLMEjv)&(Fc<*}3 z8L`p$$aGcFQCzwLt3Dg8VpCJ)T8|(l0j~oz z-w(4ZYD-knM1(Se}!AtQ2)n9!+ zOd*IIv;Of&LJpY73wnY$kce}2Ad{T4s%aJEi|Xp?k927vs`-HhYY(>>54HIAoyA$J zPD)XMA;E%h3TZ*_-J`OqnwlMLKe(Wwn6B-hsamyg;+a*eEGH?Q5r;3EHkSAjyw=tA zxN0z6l@9DJ)jjv7T5q!foCX|wXcXIOgESx*$kcyATAM~0zE?QZqS-ZH9R%VPFSLpW zQ-GV3lt0Julag=0u8f@ybUm}hNg`^|<+L?7B=K$gw2-mH=}uRbFL#GD@bCNyvSKE3 zMbn{{(OVA;ln>9Q1390ubmY&Vm_AR(C29|kN5z>kHsRj!U<)H$T2;7ysuXI^ms=CFU#Rry%`p!B9(Gmh?@ z>6b2jI_R99Q@5a1w^PqCp5NM38Z8D=_dkZj;5nzYNp@G?oLO(4i3|t)oEn(ez<{26 z-D0bDEhfteaad+-x>#gX6t3RXFAA>v?YUR{oB^MADI2}>If(ImK3~@V!QW}fSR>bq zG=)P{O3J66U&J#rGoR2+tJc;`TO*2?fQI2`VI@Iy@**x>`pwtCnqi?OUC~nVFm18X zl)Aba9;i-}Hm6UY4i7~U8URND+#5$6N{_g`0>lI4LSZ2|Z;WDMQc#bGMu~W$ZZO?6 zcz94dx+xq)eYv_ZbL9^vN1jwI$;OC0#0%hZB7le!NXJKYc&>*d*s!7a;ck~MPrcZ2 z9-fjqsgFGtmUkuG+OlwiGW=*&lIl7@$;ZqOq^UNz7Ua>1KgZ+zH%IJnzOrT7*x@g> zg9|a1W{XzX0?ZMgf97R5m4Q)+_=)SaU7yC3N}$I|m>BgJh%cu_zzQjnkofzgjZ! z0OLBQ5+EMvD|q;>?+A>=0NUpJ+cYL3ZlHHV7sdgJdNpjA_wm*lbq!aH$+P3ZUD^q( z@B8o@*9N-|L`3QMbh)0Ny`sp5d!8ghv1ul<;tDgpaDnrnRF>}tk1ZQ+yE(V(0Vl5M z{$|^N>tmo5B7;wfeuVmix8Hp0sebpap|iOJz!)@kNm_dPBS_2CYzf#7r3*UNb_R*@ zLM@V4gFc>#|A6&l2mvn?^4sqt1FB2%5Q~M||Khcfhd*bGBLpi0F@)paWQHIK-iyNu z0u1v3B*9M~(IfDKtt03RBdvr~+^HG6=z=@iY63iX>5=7P#E2=gKkyx65jcS|0UI+lQ@Dn{~K_@vAVGx4`=FCy9cFw=w=RL_`o8Ri3PBQ4wpB*1|oiE zTSj?$dgl>+&&n7Ob{;m0Dl#nQ>x3f7i--R?qr3}ue_dRH0i^)Rnm3Xw@kuyHy$gn$ zeBmQS_6TGQUik%ShChT_FTZ|jGm z^nU%61W5~0-$d!1@5anOB~_}*!OJz&4~dF%#pm5xaed^=S%>U=_QY~B(A*Y!iWlNw zay&}mfY#Q??Ib&o)nc1%c6id=J)Nq%s#Ay7jxdS8iH+U64^Kl90uIYfto4$iS4+NM zOl8=RlDhHj>Gn;fc$37$HNv3}F?vJIsbFnEBaa`i^uLogZ?GfSZSaJH{!iWElRw`a z@$@()HBY|i>BD9RO!V;Z!B8l7JZRyS#{ofBR**O)WJ^NUI3B%B!^LZTejU7d+vft- zZp?xW`sIT?zB~{?I#(q~Py#A&Fn5=7{vd!ZNGfBr9xfmq4IIeqCjt^qJ_-21E&H-+ zn!L&oWR)f|X7E1+1iBVf7T7~%TwHQZ`zvR{QgS64K$;NhXHS9C#gRcu6<87Q5FQA0 z7`QDAp$PgQm@NY=Uo%NmAW1l0BCUvK>PweCp*uHG(@wKNe@jp>vP=X?Yj{2wabt0C z2E;_LcRcjVec+UU-WcofG`(&2jW_Sc-|SFv+qE{MI`7KMebM_+NPe4Aw6HM55u9sK z5~M|DkOqeA(A;1hkn`aW^9KH;;cW96MFN0?hU3zeiG88-`o!TX=sr9wUQ)$pmk$p{ z;lY7IkdX*3X5?2&{!tloey%0)q#irAPWGaW<-NJu#x)SJtannWm$ zBwAPxJThJx<01Z4nuxPo@%c`6Hjzvw(+6pk(XF75BHTfiTM+9>=z$baf|j}-ydBOt zr-=7AH#;y96rzuH1mXV5``xOTx$L0dA3CqGpf7cJp`pk>N+_zKtd8Y0r=2L8D9Cwk z?>nT3;UE)YJ^QV3p5%&5c%eaqXMa6QUx}mzk%W=Bic%R1>xlJWe)z|)&#ZdrsmXEA zSPN%Nc?JA!*;jy61_W<6Q?yc-5P=dxGJVb#oY|4%G2%5d{l<%S+78r~cl`404H$^> z{dEj<*}bQ|*z3cBxlQst_2r@RYq5o!ugHZPPLvTFJxXgFJm#Zk_;4f~j`x)hM`M)2 zaO1P$I8J_LBY^)v!;&G@g|@aIgOZ2mAfVC*k9Y~UTa1NE$)7WxI?Ij+vI=w?+nNXH zj@$yGUQKNq6eXpJ^MOfV_W;MQM+AEO9V8cI08fRu_r8!tjDekesJAMXN)*fxpnvPvgfpoqSloJt#L=cPGqYG_&8|05;BtQ!df(PBFt0R!@ zI%f=c1HVmU5!$LV=%65}LVjh0pxt;Pg+d$nRz{MTIwFX{?tt=?CIZ!X#b!XTq(m!( zNbo5*b&@3@{FHL0nqCb@-VEmrWTvXN7J0n`luU)I2Pj;P-DfC2?J1Lzgs_GKy-yPb zldw_5%eUWpbRgG2;Lt@b85qtOSMcyi-Ov1Q^?$dg-n;*9`<94{bJi>~ixn<_7P)~CTDfy;=dcn0;_G9FrzpdjTSti78=;~m zTX&763v_w6-NX8Gz1u2~&8>~Gv-c}4H3#GUj09m_kh&cwQhMYcz-g!(E4*9~l$rS; z$!MEx@&*avdH%~w4$|KA4Ocgy`pKvrH!!WJ#~qZ{k1U@v_uJvf6|CZl4doqQ=MC6K zMn?XtQ>$Pq5vCw3d&1uro}8&++Irh&Pvfg`!-J{%B!WJ2TObqkuFxn-dv+CcXgT5e z+(u!x?ow9cQ9GVV{00e9^7F+)3T3#(0yWkF&gop_*~vkmn5$LjFSe~<+a1}D!q=#2 z5k=ip9Yn%dy`WtAp!<5|>MVFfguOJ?1T7@C7Zn9kt+BsXk#TBMkI%Q^4WNQaFMgL04hXHW4>P($FmGM-yo{i_=mvBJ@Vng%I8PBS!q#7F=YY$hLM@pN95f8n%a4XV5IGMjKD>W6tv! ziBSPXgrXl@cePVkk}?jMlNOHJUhAUUJ9ELA=9^pD@o3-(QsO9{100H1g3J@4n7>QZ z^fhFR5{>x_76hr|M@?IELV=_jKL25zE4MZV)iA=`p1H@B9QF*=w9PIEoulia_S$LF zMadgC=G^~zF0~7l9JmB#!=2g*CJhD!J*X8EF*Sc^*YMY-eCG~$~8|*wj~lQILY@{7?c+q^oan$UCy;Sn{N`P z0N>Yu(}B{OT$HUN=p3q#iPm4Smb}nI53g8z&f(>)P!b>m3GkArY5W&zmur*o@$rV# zc?gE3aFvs|$&&O4)vrK~CCu|{{-HfSL%N865~e8e$+cp!?0AvV z$s5XDg|!;i5qscMmU5HlqZ-RAwks)NZST{y21RbI6VAuzJ$bqMSI)Ql^BP{%9kQ!@ zJ#97TH6UwD>vEg_q&ypUj_-ZG)v-6hpkE%I5@olwb4FH5wZw%h;BDuLvmOh8Ga{wo zT$%DN7(EV;u1DMM!-vD+$Wf1*<913)o)V+=XgUaCiWE$d&x}Y?27q9(UTG#G_3oS{ zEiH{IYor2j7%S7cdr_o-C6s5;=+>p1f4csU5J&9O>0oOe%Xa$ix;2A zA%MvTAtuH8=h}1d5G*)U<5Cjo~3vVU_oITs=HXer1@#>oA2ADe+r6;wK*6 zdemmyu@eTvpNbipAhf7IkX52x(5G2avJeYQ2MSS?sy?BL2fY7jQ5MYy^=c5}py25u zrSZ2N!$Vo3;`W93{C-+`hU}uqX@|3yB*!?y0h@_;1(SR%YGBCx*4Q?}p-@Vl1+%0g zb~wMsf@^%~rK|+l9*)`)!u^-|AB83juM&~JDXt4Gyin{IxngPYik{;ahkw``6n*`+ z+{m>C=dO2ws$I#K6Q6c^^a- zq*`pWunc4YQ>Cf63evwjm(mhhHIdRacq4A>if>+`U|%%pm823hbdY(&exW#0%LDk= zD#SNa37nd0r%oEL1D6W8hMJa33KLAjG{Y3CmuhNiPMT(O%5~2E%iF^gP!2O09p`NJ z4>tP=N{MK4^1S_*H(}zVg6SjhuBC7{pQECqn^K4SQ@y&Y9dX%h-&8PXIBWzHoCIXu zR2MB0H{2ZpxWXWjN$)cMo?-A~wE^fA{z^6ogpTIW$Ke|o8~}U3MvyI7Gm!d_ScgoOuBy($f@4+y;ZiIoJ{XJ6z(52|H)J*x(tr-Y zKXKC6&hri*U*9KlX!+6bb+ays$lmeTbM9=t=<%Moq0n-@?yTe8*QE~#EzBi^WgU$R zl+KJceGRNg$21g<4QIUQDZmS%zRx$^h6Yr;!wAqA$MIh}XMH{kYzWW7urlShe}Qn9 z3#!Tlf$9>559$!lIk|K+D2|+_SB`Kc*K>4*769LP90of^T-pko zTO+~zVf)BA9mn_&Fr)und3=(M$~LFTkHHH|JO zDVQ3Ld@D9x(bN!`YHaW0$B%KQpyh~-CS>H~1gQxul9KQvKy!ku3z*FN?t57sR7Hp} z=^uoXzIfzfD@`Yb8P!8y!2!+t!cMibvpcyR3A8MquUX5E_o~s*pc0yYifqpZ%+ncY z+AyR5+k$I9K%?)3C@G^Dm97e$hsLJKil*s*?He@%sIdR?gVKM}Mra;SoJ+T$0Ttl^ zaR>?hA~Y8j7gy&W(?+i=mGXfILm&dkJKhylkDUYuvpJV*b-_QkqE!eLSHJ_Aic`3E z!H6n9V=Ts_%m5sTA}BPeN5rv4a#+<)cATsG&+uU;i1C`{>Im2lI1Kpb2`^>LtzKz9 z5g>^PwvK~>#RCKT{#|YVW#DgI4m+x-GiI-@M@Xw~&UNppty|08kG9;Hgw(|U%$rvnyJp-fhdJ5sVeIhPalhlfDyuFSpw`;EI??Jo<=`xS;2p1meWk) zTYw_fjmKfhOe3Ye@foiw+i$0J+62TajZmdeIDzp>IG%K@5kAV@as9CAfp(8sBPU); z-a9P2H8x>=`@1JoH|V2V2=)&07=v0|zEv{HaPhXLKS~9Q+*+?+&_l?0wkYep*{`Ii zr|EMhiR$Wls9MdMH?Q^dEa%;O_rlUo&>m4S0vr>=ZJ=2|ycnS^j3DznxJ4+c8w2D) z1(cEpU_dD&lL&z3@>;>N1eklR#MVaPj~rkaHhZN0!2f}V(4w>sBsI46$E}+;k&1dg zS-Td+Jrjjx7{=^D!#&|rLyeFYt&M^8QPm`_Yrq+NS6DdaSa%!reO@TzUJ(G`LBkXj zOx^6m6r?M6+pG;!c>G{Yh?^w22IRF^>gua(6JCD6@L}j)DEFolgYkMTuM$Zw97kNm z9q;HNtTjD|f*>jRC5j~)*bJB`N)@TjKDL$i$RxhJ4xVQzv*p|IhCkZhWu67O#{g}D z++;lJ#{FjiY7n-8S^NY%5IHn`00a=T!4iRO8-7@4J1{!x(%YQ4jLP8cQT9K zVU6M`a^@aFH1rC~-rTLxQI9Z*AAvsVN<~kRZt#kQp^q#)h!GLR;U6z_J^KY*)E*Rn zf_1^7UAU+KG4O#@M$!oLRAKmvi(;o46nkG^w>A_hl!6J5Gi~^qY?1cgyF`NXguJqK zY0pndQ$#hC^-k~D>I!!}BK)=msgvT^ zY$kG`+4Enq(YAe^TSL*(7zXQ|4UGo#WvN2cy}ON$ksO*u=y-74oh3LHlnF-wb|<;$22FY5`zlo%0O*pzD0%ZG`$(O4wT$ZF{>nWTbnMUc*n} zW!Bxhk34@F3#$eXz>MId_x2rY-M0K!u!$X{Yk&?MYI$=jiW8{&F|pf-)THSn(vU90 zcK~z~NE6oOx^e*$;Hqn5z?g3XcM)eXMdSx>2T!GAIF2t}8OEq6p$Ze=A>r;bQTeR@ z(Cx8I zG9PDZr)z2xoLg}K@D|M_?p$aq?X@vqRo0!JseCI!o``aFnIOa;*!eX+hzlhZE6Wwu zJr;fM?tiga5~X`Cfbqbs0%mKPnx08Bn*nmpRM_L7Km{58HQZg$+1zN`jKY?Lnsy@T zI4GO~uge(G6FB4TVyG@-*=wGhO*xZ(Guyj=O00g@K}50F2x}CjaxjzB^V37__n-{} zlNUI0Ldf&LND}4;H7>AVIx8w>P;^MHSS3NVROSJquf7H)$1XR;>v*eR_KortCB~L_ zX1)!jLMUdp3?1Tc-u9Fm4GxD1n@jus?Bk7;$bSWJEn0l zbs1wPI3P5i_Pdj^N9}W;Ty$hJd+E5g zCR;Ry;BNqitRwP-M^f!y;F1R@;%HT&_;U+IypIhA9wR~4jz{fy8SE2I&IQ*UPHwA8 z^TQR+alfZcGdqm6oX%}^^l5SQ{CUH+G(i7{ERq&76Yy4*C|AIPqg7uk3abji7a15Z z8E969D6d0`Xa%V*5U1eASCP43S&@Udeq_sXo}>9ui8X0ueHYykk&24kV3>%Pz%y1C z3g6%e+iN$05mzjj03dDnq5f}#_%~hUX>WJR`#+H3wkBrl)|?XS6&l1OD!u*u7n$}w z^u8lxP!pqr^v6eC)1EBH+4urXLJ8lcPRIoNjQfIh!;$}(7kU!)T5~#kGQoqepqb@R0FfwOnsI&9>HVks0X-RV#f zA~!_}+F#6tdt0&G9fk=t_aj|A0+AISk8<2|xV4lJ3-eUQQ-WoeyHmAEe$V@oU-iR2 z%s_Rq(Ul}LbBrl?atnUdk<|IREY6@Dc#OE%w`29cJnO#hDh@Ch*fY*F9=I8L6&Cn{ z!i3EHV?7$`>JI_)Ov51m{f{_#hWf6--MHWL@PAD}GL+o0O^mWGU`3HJRJCeL&cURyI7Oebeo+yJL6fg`vKZg_}=z<-vyST)j2u`&?DD(h&m0K zEn7-%*_#^hzjfCg6Im0yikKZb8*fxEU3w~OJ8W_oOH`s-{D|h&h`d!;lcG=tG z!oAnEc;oZs%z}4yuFk0dp6}{EZLuvqC$*N!w;xjj$AXE0!aI>L5xM8 zTB{DyQcSND)8T>!sj(Ju`sWC60zBYXVcDloN{FT85g<6w#msbBBus=lq}=0v%|_+W z0Cq_Wt{AZ)*1Ovp@}=Rw9vG-K)N*L(G~G+nR+|Hc0qOw=2*+s}rgn|gw~uR9KNv$O z(K@2>fykeXIv1v}$I~;CE?-4ay4ygntAZ=+mYFw?=z;N5iI{!ylwHI9mDE2C=;6XuNQ)WCeds`;goFcI1oEkoT(dIYWTE-z+}^_Vx&;!|Gtb3IQxCzoWJ$eEw(=r#w+j-!8g0}>egmEphHo}q7?XOM~0 zD}dX=f0HuZ*Ee=(D6JT+8%RJ=PRrY1Fm_nL?;A@s`?0uQE+g;o6nrxxC$Mmzx^;sS#;o#A~yELtMCs7C?u{VUaVWdoxf~;Rmv@vL-#uX!cVO>+;6+6S(i%h&{|VYm3fe}p-hoxOh4XtXLMzZp#Q?E$|y|&ZJj9Z9B%TP18Mi}6`?+!0tEII+d>LB>Mr#4%iiZ7n;`og z$5b{5p12dUq(V^I0ceH@+aMh14IW-bMNqz-3w#YlPQWBKXa}NlcYCR;iIv&Ezm5k~ z&doc=)Rr0U&J`Q&T=dg~s);WFg{7z84OFu}RS@5pg@$jbD*JVDR}7EzL=VV`#FXcsg5kO&O~AHM`?g12vZG!z9OLKt~J z=8i{fx@w=JgXxvIG0p*hBQq7GZ%UcOGVc8gw!boM8%2k|i!w0x9%XG!2fm~Zsi6G! zBLby9WcQU4YAE`WR=J)QmGgTc`!J_kZ^w)C=f-I4l6`1B&*i#FF5VCy4*|X zE0CbhV?x4sg?@OHV0zSkWNaCYxya-O*3sOTwJJ&pAC!c~l@@x>_$MTZhA0n&FV)b9TO_64stGo5%d;K*qKl81I4 zT1bihII?_aZOqm)3P;n7B$haMILBUskduD%L!!UfvZYxjA}4MTVT;jCP7V~uW0p5m zPr0I$J=e5w*Hu+{@V8AS*Av6l4X4T6POh1(x_A+JW=gEFLLCLC|53QD2-B@be$`Xi z((ccY!k;D$g#y(j0Y_IL{QI7UUtYyPJeb~+a(9mwo%)@<)58@rC6sXGVXq^{81#!F z^VqxNEv&eYR47Pq$&ZS9TnE^i4de%&g8xwk47BkpDJn>GA_0H@1ZDRCT&cr|!Y&02 zCb9T=2l|?Ez`hc#gn0_fP+`Kfe3NyJ;I>%Y1cOQ~NKDY`-gic1-GP77ANVMWI>fcH zt<*_em&;{oXh1V(v=f^r=T`n1=wwepQ8!FsKj1rn{EwwGCmXL_>s1l;z7X1jx-n$* zK3sxWmWm5hT)@!L1{ymmvYzHvw5=>L(|7R58G}!F&yPR)^2Rl zV`Rs0R)1)&QkuByJW2{nt|*0cm`y}p`gRd=gd?HTX+aSBHV{jZ5c$#0kCijsy<*G( z#U(21&K$wkEhq})r^R5y0QOyhQ+z&O@(HT%i>cp(+&2I{`ibsA{73!*IRPpR%FIMp zJfwvCKW`oCNL=C_gs(9L;`s4sKn$E8rHQwnP=XSXliRwTR+LydW$&AwioYM~&dT_^ zaxr9V=&1lLFArpTVpKwiQJjJuudTiy4(VyCc&7Wq8-?)yD+X{Ok_-}v;83oM2)n!~ zQ!qXK&r>ZXI@#kmVh8#Wq2OGEWlc>A=%+GRbva3IsJD^);0)i683R8y3~N$bRU8_e z5sX10ZHGE9Vch1VHI|T!ps<97ibH67h5{vHAX6xz;ABWgyjp-sau^K;--n&NjfQ8K z9LgrLNriidB7|jLoeJJ?IQK@hBv&qKL417X!lL{X_HdjWIj_&i-I3k%cC^`nlKK@T zUw0LV`o3n=CyW>2A_`?fR{HUM&-|id&o51A616EumeOSr1UGVH3hLLC#vJxilspw8 zmm03QRB4@=nUdANO}60!e~pLX4wQeIJ~s8Lvvx>MMoezkCU`p*E5qh2oxuD;4*G1S(5Bc<9(H-yW`SG183!Y#Q+~%4bu94B;2<2Uj>}A@M-^DM~7zJ{6&Bl=+d7Bb6SgxcIGyGG%{^GD;NvcWD8R7`Y0( zx+%4#!Bi{0{)FG>!1$qw8H$Vq>E9Ruc!uG-kFYFAx&DwmiJbGz=BxY7+WX2T9$MD_ zXB$73;7JNhAdn4$wgn9l{aGG(fiK>~x?W}^~fJpw|+6*dcKz>)#+_WMb3`i>q zDHp_>|?w18*LOEr<1jLs9$P^%4bGX z35)L%V)*JPZ~-+5+qPq2q*S@-35~I)PP_hl8p9Pjz-Sommt65N(Ij5Uh*Pw1jYQhk z7f#E;sqsmGnmTv5qF-l)E(nhKa1>7E9Od=o$A8zBUT^ufSP!kqQ|{kYVtRcB2?FHwo7KEQb9 ziVCZ{#?g1)CZ?*cdwX_t(BEJu@RYmG5cm>cqKYT3j9msEic|f1WCxJ?&cq!?6Nz}C z6(7M&MV)}x|e{hL>WRtwmp^AqRidw} z;*U-w#F;>TyI25pFdmgP%{#vxodG9M?mN^KjRtNh`GTRZ1ytwKS5>TE1OQ6C4d1-V zk5M@fP%nbW0rZ6_n37STG~nw`q=wQ}(F2G6M?}(>`)DW|rd}*VCkMm=1oF#CCh?3i z3`D3u!Zy$lL!~U01w%)u(FvUi=v>3oA)OTlO`NJum|h_AOHd}Lz=Hx7j0esMg@?IQ z?VAq#w`SE6<5!#ucbnEI)^#mxd!G?SbV!B@v|4rA^&E35uFw?Cagu9R3EXy>s-Kij zl`{RIH70cH`iW99L0`?hEGLGqd|qJcWz93WFgMI|P2q5yR<_squX>_+gSl}&{U31i zjp+6HuTRQMjNf-TX`lD2bp1KjWeREWn>Vp`HQRodV`rGKwZ3UY-OP9c!rzVUX8sb%B++F+f$ye z!d|}l`g0?j(o%dgWweh7_K|?iR5Mc5!+t=*W7kxWx)Hxp|E&ux(H&oIq}p+=sic^d zKq&D*+cs|tj~c?MjAtdH39e5|P+hlOcs?-1eSWLp=hsS<{iApd!Y=kuPSbpK89D{$ zY%)VR95a3=CB?J%V?bYB{oqd2i_-04ao-7$Ab|{MQ=B^VCDjd54xUwM;&!=5stBq3 zo1j)$ynkj=ZA?Vq2Vo#8k#2y*-o0Chn?;|3V59mU_b~4u z-FAs@UrAnX!&%e>V`NQrRn-T&r``Aac;5#gV&!mMLXfU0%8HCuhC1J_g>}x6x*6}HD%yB4B?feDS1P^yP z=}Jv|d$t_3R?IEI6C~Ry&6CrmX%A<#FLV;ku8|ScF2OW z+*vtktz_|dZ=d;NZrj`!Xnawt0PZ+QnXLwprK1Sw`73?Wtn;y+4+c8ma3d@dB zl~z8%S6|2GJ7A9&u@fC|aB>e?+bv6}Di!SGOQ)C>ByY|{wP?oMw_9|)?9Tc2wPror z`!wVk@1VeCGd>aP5W&YcaE#IahvS&?9BA-&hXD9ut6(#^ex^qbePzgM(AXigdNd2y z&!39*>B}(qh14SEQ=tgYnsYkkl?IFR=7>2jbfjg5#!RB~gTH>H;6+><`-Ip}IYnr3 zizpFg>1kx<67lbb~XH| z4)=s&w^;RBkv-y(jpMkjGIE9`bx7bLW%vEr2A?pXc@l1DvSB*P%4vSZc1#z7k#1&4 zeG3Z5F%^S&7TV&F3Ys?dRy0dU<73gVtT`ED0muY!3N9z@L8rH_FdKH-`ws=ay8_YP#f-g$u!Utn=w_ocght8YD$HPLY*$h+ya^TGmy=c*J=JLxUJc)O z`E=5L6J=SsKoJ-#W3e{59_NDf`vU>!x-fVWUH94iaEV*%9*g@WD6SEgbrclY)cp6T zF<4(P>yQm-8jNa!_Exs!3~eoSRyQQCMeODvchQVA9keIeaoUo#{GP^czG$6thOh3x zY2$Esi$YD>*0KR@t=Ixy-2s+e!^#vfL;nZG8pBzCytC@I=Ug45W!koey`L|gv00L3 zzvS70`EMT!s?MfpB&Oh^Dm@HeXDh>pT1MghtoN{8<`%Jsr2b+=LHRX4z{~Q3rCO%X|iw1&Ge$?B74qjD>ta znY1_{E*Ty{Rg4k6JdT?zX|Ir22tj6P;H}*6@~}Au7Z9~R4$yKXK+3RMO6ao z??M*=QC_5L@t43w(B$M!roeV-dIe=|qFJ(IjQroi-)7Ko`aK!{{FrXexli!caWOjJ+mI9 zlp6ODORuEd9TI*UI#0>JXcqK4O>@7%7b<5)V~PI(Rt*$c#9aV?S)n;uSccEWDTi4i zxkv${n{k6F|Fv{pJoBbG;jWw6<^*xSV_1>O*w!UO#g(YD!)LfIM7uf=?$y18Fo{)K z0rHzK=0{3fVeMH9wt6)W%!19}48SOBf7X|6d}B{fL}rX9j;GXnGb28S&{{dnP)+`UQ8q9&t7AxTh^1A4f_M*Z9lnUfDp4Z*T z1}y$bngX>ReysCv!MTO^Jh*PM_A%gyb^k_F+qW>8JF0I?>gSG=6?U1)J{yp^+VS$Uw!rz zug{5>fEXK470?W*87P8(Cndj#i?;Rg+_grSDIf%S5?3Rq3f zU^TnCRIrha`19EE$w}kqiHcA8w{9)`aXUh_Wft2kunB;OU276Sn^NB+dvq^J~e=9bw60k5XHp?@3$!g{`2>-V%-tuNZKIiaK}dVgYc zb+%?aR;ySLxpejvy8MYQOIJ6pjX|cUrUuy*598Hm;o5!IN;15s~3}X%y$uRPddWIO&6z!MWQ?+`pZ-86b z9A^{dkHWS!MlM{VG1wLtZjzx0a1M>GiQ&F~M&20BZ@3yz7I)R#VQvYAPC#OW%Yk~i zvocZ|2lhDu3mgN&8dh|a^?tlq4W(0rkX{gVP|y%KjK#whqAMs7#N-JN`E*5LvoReK zFVk{P`#q&~>8|lsm`kCQDVYVidCZk$))3Q}5o(|9HLTEV0(!(voU89ctyG&m1y~0U zgD!OrPJY6>eK9MpPg6HM`LRV;_2SyT(iyw{O@5v=)H7mQet^EucfY`hFkqbV(rkI` z?-N1fwuUW{(phrS+sv-1aEg5JR2)&y`pbi+0>O*9jNDmWPw1efbCCMFKtxql|Z)getIlL;t``lQsn&WXOb6ZZ+` z4ly7N8QjuuxX|)DO;N>Ct7&ToFy7mu1UX+M_ThD5I#U9$+}l&k)*;S!ClJK|GKH9l zNho=F>lyclIr5V5HVD2nxa67&Dv`uj8AXzliE`y`GNXkbH6<d%A<4?zI~mlIP|HcPUx^8}QSS+Wn@ zkUp=ni z^y$*}y+EleSV3~d!@B*KPee7?p}_7b;AFJr@Z>sgH@DdI{=MgPyrh4@CA@O z5)ya|O28two?5w@jf^nk?p7Izi35p;Q*25y@*kuWOG#bMs%m(gF5xa0zgz@U^ma#4 z%-T-KTwJ_%j`&0T&uEM|0c;F?jEi%8UiVA~5Jbn3IR(rhOB7i>#ye{8zoUc{u?m$B zjp!91e7r^E1W^6vi-3Jb{?ROaK9k*H&=OUcQI0Kc=tnYzrQYtWf>QL7-bzUUR-~D7 zm=D!)R;5G&VFytUlUkJ(sRAuA<)3t&n@VaAz_KgbXO!r~a+ID?&t zjU#_fyc5J&b)B7v|0I8bv-+6lol1T_rfl~5>nc_b)*uZ8r!dLh<>{);7vJtyTgk>P z8iZthw6wHG4)WX;-eGCdGZx>r=gck?WFwTM7vh`W_&mFA_4je%R{aeNL=FvpAHCy^ z$kV21=?xkN+v=|-UWU`Q!dclu10f043wWdxA8-HLws8wS94Sf%4mdpS6YjlL-@k~` z>F^C}EI$YV7;`J!xE^17e(C*c%b>*NZ>jFT3K3o+HRbYLPh_Eai zU*KO@3oX`-K(t6Vid?On+8AZTuctcla&1pwIgLVm7LBz*rASe-{!eAXC?MPpw{3DB zq5cTo@Hfypmtw~O7#+N(lKVS~k^Ta>nW)vcAS&vr=I$KZO@$9GmaO*a%||VseQ;N; zZ{Osjc){ThP*X#Z{KdQo_ka6z6Q}OF&Rghy@n#HOGK9)S?=oU?WauU7j4tNnwK~5Wh_7~ zwszmG$z~=&vb*VP$OCB2S>x|I2Hc;&EnW-Xj%WT{RAFrz>4&nLLL&Zw#sP60kdnG0{gmk{UV z=-x`&k6R1i#d)D9^Pnma6sHw|wEYh`6EVl|rk#~G`A08yc=Gt_bAu6T9tkk2g278X zOr`pH6mGMKU`y_Gg!^#th|L!}k|y)77Cv{2lY4aWr2{s*((`*qvRB)6N98ktDV^Oe z7tWcl#+rd$rzsw2yq6Xpd9|=Te3RS)2t7R8(v z-2^&}RHQw{iCsm9m2^SSL${ujIH%f6a7eK$*^)#w)?xb?pAOsytUt1w2mx}BeCW`Y zImHkNd`0M9KN{>tp+XvC}yCi8ggjHHc;h5!5!dpC@R~bvw)~FTegHVfn~qt z`txU*qZaznQIGUwZBu1S$cy*wcwa6XVb&%(l1ErSGX|D+3n7+z zV;Fj=G!b=bkj(+fsqxR+`U+FS4e$u1p@Qier}^JBurm;gOW@6|tSOplCO`4}51fr@ z$Ao1*IDE;>E*}$T^a=@a=Zk@A&G7ZBOF$!y6w(8p4z=U}CoF|r;Ii<4r*<3YHG1(s4sTEY_rHHaalamof+bLzC0X14^xyyPLNrYz;j=K zb*(SsldJjtWi$(NDmMFa(#Ko#h68=QzwJy}cq#3_hR(!wO#^ev^t-L+9&50Ex^bGp z$9<{AHIHSZq9iX$-_l-u+a01)=Dp=B?VB2k=99L-cKDGQz)y=xfsiv2;suZ=`O@~# z!QlYI`K~t(uq1-=qmTXABQ)?bs7&4B|Ht0$#FW`zd%Rk%7gKFTZeqCFjlSTGYuf)i-yu3lTifJ<(d)5W#$$Zw50x7|u^+y_l0fXN)uB17v@FP*24c7wKo!u@9>ARc zsbLSCvD%E{M5Qe9ad8L6O_!As#GyJp> zFc0AM`A)#oxu_VPcS1*e4}5fO%#M$YFHUZzAA`_)Cn~o>LYy)Jj5J@ii;2I4+mfJn zq2zhUO~m$X-@*_hs$V{HjhDdnA1_zJ8#_?ev* zdZBs>vICE}Z@Pbv?oPI~v50pygcudYSRd$F6^wIB7)Y zOjq^l{V@|D4_s_~^=y>!JXs}}q!y-(u#b&TOkXagwr%m}MROdQduJxkJMQ29wX(0n z;C|V@>}Pw!wkcimYU)V#?60=n<5Ia3D1}PP;7^exW?*m*`UZ?hcsk2CMB@K~&Dhx@ z6jded2SG=UB-fpV-H>5aoe`Hyo12AL%bYoEIgM*TV0OlSxUb$T8oDCTWUbME1JQ8a z#ZmjLIbfMb<<3dkm#^$f$sc#>VoG1l<-71VbFqYi`Q+s>~gn2P}lQ*t#6;j?VWWEYlpwjO!%m% z6h73kG`7EXhUfQ(0n3j5P?Cf6z{Do&|L5eGWkPab5r8WY%1AZw7KRsdaexF8Bix41 zxYkADSAcVkjC#wPgl&}WYC0|UNDe8mRYuF@B>Hsz4^v+PS998iZE1+f(t;2zj!ZII zrIJDiom56fi1w7UsZeRN>!3Z-Zcs5K=`AIdNQo9i%d}Ax)4ph5zU%%U^L^j%_r8Xf zbN=Uf?&Z3#d*jir_?6fLlDOMEenDha@bgFAPY4U*e}F;=(FlL-#=RmqlH^0yE`eR4 zT;l2uk~wn&eVy`L>j8tWxfRyil_Kx{Y`QH~0J^Vfd>Oqcx9F;D#8AHbWL(8O&F4MB z@t?}FjrL^?n$hglqx(Z0O1^$xx$UFB#i2j1l1;PuGu?H=e)Is*-7tR;yuEI?Cr;Hp zaCee{3%d87)axW22!;jM5|)^v*5|xsRrkc`wwlO7GcH7^KyiG7-bAbo zx!LSD=55L*qeL-Nk(3#rg%GJVJ+eT72B-etzQ8I>(Yh=5>} zxNQb=p35OFZU;Ma4`U#&F}{e0LeG(w)h!#Yrkl}65JJC1w?&Gv-rZIXXWSRR6O z`ulIpjNP?VblV#NP*x}YKJua@Gxp!V3rjNhU^&XrEnBtYy(~LR@1veNtN6DEHEfR3 zgTuKpmH$gj@&Bet+IFg*SOox8r}xM2s%|aHzyA9AO$yCG{bB?rja~)-^&c=62NM2< zT||Hc(&6UpzjfyUc06i-ci<1oJj=}f8vEGq`6>ggXCb#_F;AL*9#)mih%~?{B5pfry*}m^4>{K z2bUy{+oLNpr4y8(7a=2u(+8JesEdpEJOlB?S?@Kk=9GYfnm>(^1B8~)O=G(6qwYXF z*S~hw!5_keK|^0dh){2YMSl9)70S3883+^oIHjt$!&3qoKc@UKQySL$tY&B0$#QIB z8(cTh^L)_5-RkVU0|&f&+s}#}Sl<2VR;#M_7yhn^iUU_JMBFJ_w>1R8#{K*K#WEJn zY^kXs)orO+sdGUp^C4K9E|PF)42ud%s^1*l2Y&h`{0FeesDjdkJuge|fT+BY5~DUQ z5frmwds$>hkLW#nvQIhg4J*%|Ie%+qPJ@ub!rL*2&x_vO7BP&>=JVI}gAY%R`KqUC z>&_{vGI)Dn->0#**2QyVS3`NhUtv{{4Km1Zij@DSxTU^w{RMChpl=fZHq%7puNd8( z{oBU#cXat)=T8X`3`JAsZ&C`|rp@2N;`^h+<(OyW$MmqeLGDfzr@Wro1yZE(6M&eQ ziRtH_a;*Z}c#lh%*FYo;ffE=&iYx;C<#(_Vsx#S$ufX|GS_&GOww)sg`w`$lf$>8_ zhv>FZXh5p`*@)CTFk9(O{rQdjUdZD;w|rbXKKZ1+)6Y3d`Y4vHfq??lEvB#(`jok0 z)WzNDYPQ>a_f{?)XL5T{=H*UPQ#BiHAxos{j$P0LSkZ^$n^0L}57#$75jesx`$#+YhKZvULZ z=HFcnzVNB7ROH4;gz0eO6_nO27EHJ*FC@N$8_!MMuDecVu_#MoYDmyZiJLdPOKucC z6KI}Nn78{6W8IySe;W>*T=OIR{4ZZivTQv*EIW_U_)5sg7s zll(-$YSGE{hpyfHoBh>l#;jWlhWCAG6$#y(vW8Q9*zV6n%P+Quik9bo`F++V|M~vs z_(PUjvXCbUx#_Pl_QmZ_@hjDt9#j42OB77w|MncKYG%t0HWc-m$o8MSd@xV5B{qIS z*x6)EL2{u+f3>Zu+du`<4sw6%mPL{P)Wsq>Md%TCvV!cM4E!W68ifSNHA2N-*AH){ z@GyD4pwoHiH*@764`hgU2BC~0T9nWyNttXJ`O#+Pjark~#UVbgiYRUYhX8hokub*2 z;`S$Q5j@4+TD+z}gfYAXonP0h`7=L5pa$Vcs2f%kz>dfWevViv1=%51OLz-5P$60$ z{t!9S7bu!j*Q)op=oYV5^y$F@qpqBwt6rU7|IyyNIL9w?M|9WGuf2zG|Gm~* za`yDJoUh`adPypD*DEeKo%LVX_D9D=K3=U<~o5ClRdTRdFn}1x} zTcvt@WcQubRCpm~{HZ=5-u0{Q%htF_IZRXJQ1duE&cA3Dwsx`O{@OUYR@_&<>1~t6 zldagAakMr!UizcEOlzDkP2v0izrxL5H@=?0xKW>ZZ_ZA=e0j$)^$=7|tetEN?rZd!6U{R4 z`*+PwjbYR+XJekNKR<>4h$s=JOzwh#<{@B^euP?~sU0Vn${`?8vvBg*jBg;ULr$2k zdUz{CEIN^n0HyJ7Ha?}a;?YH?$8F>$%J9FRaVbm;i!m{H3HWUgPHAxOt00tV3^6@G z$O5=78Q~TxpX`>nmO(^dngad1<5$8PAT~Ysj8bG>V^x!dZa}}72|fDMcS6H?^4QsI zBO@S2{v7R%WJbO#*>d!o{`rV;-$8dY$4;qjJQt9XI$Ux5y|rH1Vpr)_b`;CifNkcIb8TUv#_u-_D}Q&`x)Fg)h;6vCq!?Um^kwGONF{R%mXXfUi26wYV$A-;B@81{7HUZ1;IriOF)wdBd zA)Cc_1zgZ8T!%|CC^6or{(3ygpr=APvEj)U*`wO01kMR8-uErJ_f$ojPu;iAxG7{+TytEyu=nWF$+JC`3urLHEIc#V z0RND$*;T6l7aoeB{HQ(O#O0typ9`V$R8V&&fuh^UJN`32aI zP)U*xg-()uQHeko48ulNjsOoV6T$-qt{1Dp03p9p?$hZ;toqoSgdGk}xo-~lYuLn*xxX*>p~3k z<>``q|DL>@@Q8I`o#h%-l8U^JS=|R7F{oedWV`6!ePdTP+_e*}T8Ao&$?s_zW3Q|* z8E73zUy`O^rzn{) zOIYdVK&FxNt~LF?T|P6pz0BJD;giDG|K=R5AMx+0To^3LXMB2!wc>oM%{68=mck>l zMch=0g&1AAg@4njDSf|g9GTQ|K3Zi}=!6xtiOk z$eFqM{U-15Fg1lUG5UUIr>GStU7z6JEU!6hzLnx~QQ_%Rr`2|d2HM^{A-fRk=eFHE zeCg(;1j~;P_IRbV=3WT;vaaE-jBf9amci_P){A$7lb5FG&Nfqq6hbh{eCA>Z2200!_1)};jw(b5kBhkMlBJWFJp0$U~P3v zV;GAzq{JkKZ;|19rxIn#D8GqX*6ePtp`=!=b}T_bFsJ20(8fWrD&LMO zRdw|ybgW^B`J?R7h1uE8E`D9>6oqS#nOJDXR}3y~*BHPM*o_BlGj{O_@t^j%I47t> zW1G#HX$}rntM<#L8wG!RviQZjGsdz3Q#hip^kTSD-$vu?#Ya_lns{FDzrp2qco?7{ znflIo+RX<>Y?<%}^Am32U`~sia*2xK*tkwktaX0cBaQxY<@nJt=d_%h07q7jDSF55 ziRWApdD{E>jYhTBr}iwzmOeE>YM-~&9J7df(kt-&cHq@xhD8hReY|9-e)E{vykBFs zHAz}5omyzEZ_}rSo3n-Wy;l{tRY-6%HYF=xy3ttoJkoj@_cuI+pw4Ej#6Fg`STeK! z%gRky$78pJwM8B&} zti0s7q*gifutVQw48<2ehkxNJmU6-ke@ifr#*sMs?hif8>iI2nXb%=&FX1SMUe~#a zHEK&BCTNGJ4_37Cg<8n#NJiDAvP0;C1o&ZrxfD7#Jv8 z6jH*H!nEugU0zaW3}+{ZNS%?E3MT2%!_w{J8N-7T_?V#&@q&f8_fmEG^$afmlLxmd zb!}WjAwhara#^5w1`9?$agr=sJfyXC;3 zn#RGS-Wq=t>6Qu#2NuC(QtQiCMdou4IH zuz@w=87MI4O|IVpSs?;J!5r*F@|H zjM!25@zr(Cl-0`;L`JYcF7p_7M}!=0Dy!*m%qWZbtIG>h6=V`1BRIZdy-KVp#_Bs# zSe8pD)N{XWbfHQBtMaPhwctS1j>;caeLWI0A4g|HEd>PShw(u1sXS<4k4Hi>ec z)1!bn?@xOQmp;W}6R~B>6arW1v;WZj3u<(}#-Y$b1v07>Uru$iK8+Q}@u_Um7MoP< z!cUxGj0LQSL*G3XcY6pMdT<}27*%qc3u+hlz!)Fp(0#T`IU1K7S*Zp>TJvgQrUF>H zGN^|m5y7Sx{36#yELA`Gs*4ybp^fMa-f;@Nc-ABT*4ZgE^1jiIYj0M*`5OAwm9uR7 zk!0&78|WrlOj=B~G%X{UK4XaO*E5z@h}N1ntlR}tsk2%A?z*W8jn$u0-GX|uui~}Vb(8#`5Rc$(^#kZ z{7ZBzS&r|qlxpnCCJD6s;`p|FpKVg55brD>Uw*y(B#tC)IHtMT3J(qog(?*qAI=+S z3c;&79?o1Q^aHCp9Qu9>JuG!5%)u>`We^G@DqQ2A6?bI+Cm$I3S4|!6IF69|8F!?;? zNusrqza2mR!8n|ZRuaGU+N^}X`mSBl&X47a(_I%0P zn=t$ea_oO2dfF z;hCi~Jhkv~IhIn~LFQMlTTA~`Ugi=X87J>cHlj~yXund)4kk7HxoTjwv=S*riM81F zg_nOB9UZO9^NWA+LYumVQ51;Bk>nmH2eJEFc+Aaakw|Vw$VD6v*-KJXatW$H7T7D^ ziass{jJpNhmX?*p0d!#VkrGQveR~qfGxl_&cwPzm3S?I1JedgSvUMyA6X|PXwh3hn zZ-opcL8%!F1+TD8$fPPHlDTlQ{+Mmff}+D0k+v(Oma>Ib!MqVh8OON^bH)@M!Xjg= z@$1llX&m4Tn_g2g3tcZ&u&JDx(r9f096}b*V7Kn)9GxcR3{Me2m5_qbkOG`Fi>%?z z)KV{On`XJdLt^_4%;$9k?WZ9y$9X4Bwv~PW-tjP5K-^Hcp+9^TY~=J>*lDAC3(}1o zcH^{*^^PQ0+lXy1bh%xrOD2d!4Z`FK4g36Aa=mlI?aJ5GnnJZ9Si^NU9k+EIMl2(2 z_pul!zH2b34t<#DMgQNe%d_v1r8YWE7==>zd2OrnnLmp?6Ks&Tl$EWpEuVm_e3qf)q2`XmOS_%yf%QV)ETS8(4PI(K|@1_L|{ zU}@g*zd`PPn-H#O1=m;>zWu9fvAlqWNB1chP8(X{>8;sH>TV+>sGM#j4V5-u3Qfez zg&EF<6Ip_1JWOu^pKTQl2}n1BFfJ|%p>KUKhHp`BR()SB4PC%(!#eG@KZJTezGhX+ zXv5j?1h6C3iCli%3$6qI!Y^_8b!7-}K-0Wh{$I2RLl6q|nYgTPcp$b0jHV6=#56@az1956vL=G`@NCE<10Z(@r&-r~c z&RF(^ynNsa#zB@8sF0G8wSa--orY>)Nvj*o0~Xph<9_fD%^vQ~KXfg`w!qI@v6 zgBiYmmlP^P_92d4I^76bfbRzeUN3L0Zb38emv7(vX%;;?zhQL^XTy9wXd_#Jk4F~1 z!dki%o)rfQDTgeQcZYFHEuD!ocq)$Vb-P&aun-hhkPb#rkgzwb8^n6THtO~HBBF7)udr%nY;ir3qT%9M8-}r%>NUM&I(TXT7tQ77vHczIhbDTE3OylR?X9 zAOnaLTt0n90LodA=!g1vtPjT2p@of6?Ok1cl?R+%20Kk)6#(Zx7z4T0>NF*-PWq<$ zvQ=|XEe}g%ilBQWEv0}!a7q{y9sQsUYdZ>5^vUN!lHn_`$CRWTXM*>#f%5Wx6Hn9%Rq(9zdN4WgTy_o>h`WmQqFluAxKIYTFjgu|^_qn!Oa)YhC~|PL1d4HS zWUg`Q6mF&ITSa3DpQ%^Jy3PilFf>|Eo^zU8tUXJRPfJ)3b8TN5X|+zSkl;610d#gw zewP1sx;tTx&NKmjjzsWz1=;|(WW&^VB=xwa!z=}b8D|}@)8b7yGif}muE5MNf zTSENj3bI)b?SQ{woZb1)J=BMt6%n+S4lAJ@DOBAAQ-De~e||C;@0dH&m4U3miy+vb z!{x#J)KU&&#ub1P@L#w^d^&PmkOX=PAXI#0600N0SDJP3so}`DcnB6;6=WDH2Lovt zElo0=l0RZ8iIU0YeZ7sUBY-)=n6GVit!|*yU3?NWqg5#`@Fq;}T(Eb`@e2e|>gcj#tFw zkIA9AGY?u`IVJGqlkGJ1s84=lr8|4i?=;)mvpGU{ z#yB=*!Of%?I70;p93n+>Ar5I}%vmjgdCeI1sk7wwu1Vkn&N>QcT#&eKLRolQA&zw1v0e#X+`;(1$ z7LN%wZXs|WxQc@WM!`1-QR3gEd5BB|H_M!V&_-U9?u*5oUTyUR)7N%*Tafo zK;pP$>`!K7I{-%q9(nG(d1~tFv}1s*BgIe6stl-v70OT}JBX_*Dk`ENwXa2gpm$p9 zT8*vj?NYwm*xiAf4tg{xqLgeK6K^0EC*@-;CxT@K_c)?9m`Z7=A6uSowd6Lwd~ORJ zFFgafTjmFWYsl=vH-c~3#&5+qFHDTPu!+Afn2}cIvalh%oo{-RiIC(j`>)hghAmV> zW@P;Fe6T%qKX^SW@HLejDHu~hoKb=ZK?+6!Vb9Jnh7lHpyaO-fFb!H(!h8O|<6M5q$o^C^G&a=$SbI9*Z~ zPE(Q*9HyYdJPKcV#|v5k2F4Qml-D;teUszm{3tuS$TjOWRSuw&Ee3l%c`lU3sQWY` z#^mxBy0Bo7q|UgAQ2+_0hoA~VU($`}JHI9H;!+3(4xbTQVPMG8@*TKOI7_(G1QM13W}B1IF@w%KxQ{xZ%~zI^SQ9EtgS zH*QqE-XS-B3cCtpThVA^8}D-s(T7`G00SIV_{8)^wXu3oTXiIQlGL>3R1b1VqIp?yN-Ytk)v3exCM1S{!A0L%#@x-vLkwmJAf667dc&|#u{Zs|1bCG9f^ zWdft5)Y2pAQn25KBttyEz~Y$;*bjzzVr~KPy}W>!nAp*yM~M=G1IL5`1%c@%RW25` z7mMUfty{-DvAaNr{g8=|0M>w2ws&jqFnHI)pRv7G_C|9OzEY^dPzQFVdB-pB_3igd zQ?0fYosm13vnbQEZDCk$7mff#H$o=bl|hp!Fj$gydlrfN#XtsK(8^2v9~0-^M6f3T zNC%4pEOn#u5@UBwst{2L=Cc_rPA)!%g`h(7H^$9)XaVTy$^aF^>4B9g1}}OTDwo>@ zc?>rcy>Oa92q`{8u{r`s9v;cv4!owXguwmj3c=G7eEjkfQEX)uoC!4;=T44y_1)d+ za|shZv~0oa6kDc#>YAMB8Xl?2E2DLPG*BLOctP-R=bp7R2D8=e{@pi>7DC&Jk(5b5 z7K9+QL7F9X7*|I-Scd-Ho2OfK9Dr=(?L~k7m_*O!fb~OI6?O<~!wwg^SQANQHulo2 zn})p>sz_QqQ`x?KJ5%B>KG`E)gGJS1+o4A2!)cB8i_^k#hR;1k;;SkIUYwyJ_s!xH zpO>y#I1{J?l0j{>eq%*`C4OT;d6%W;PkXV`w^uk9tv5%<3+KF|4NTO#M)ONHLw4Y9 zX^h?weLgTUI5ZoOqu8&KJM>bnx2D|p8Uhp!PX2>2S$^SKF>&#*$bO%l@DoI2zoTe` zTnoTHNW@YbilJ`l+-y^)a0bMAG)$!>Vp#=Zt9gt{0*Fh-1CWsN%pUza5nl$~lDh99 zfD87T4tCZzuhQ~7o9*895QrO4#>>kKa{E@`#U^?}-$A!{i@}CvPdv*j%DvBF7y2cT zA_(m5?SJJUb_w-3{8C1JdH#TTcJFSy6`qIQAJ6LE=yA5nDYqo^;G_D%ev_hs6Xo&( z_)Lw1kORC=5{#t}KsX`NrvAVy6?Zl!8Pa5++8b=QC5Brsj;RIfS>m*ii zE6@_)k**A!DWP*7qr&h_T}3K_`J*BDF~s@ohY#A+3C#IZ#2(-XBQS5d2IQWI7Q|gl z$cE#2v~LYjD#Q@N#2_YtZf)BBE@>p3sqIs8)^sF=qy#V zJCY1$X71d%&?&eQ#V~hBY@0eh^1ihZ%8|vGfe6alLKOd^5Hsq3oM_lgYSL$#yIjQf zKYskU2sRWk-B|17cMNuU?&;fqYwyrMRk?*QH<(vLBa4m`uYxF@21a;yy2an>@;YKf zz@3>J*<3gWE)wV;k8nhK<+NJm^-gu4Xh=n-*WlVV&@vDOd{{xju}bIhh3Q5FD}aC% zfhdh-_1WEUJQ%~PO+p$9PKdw)4{1U|)N5fzfoEZR5Pl(kLfjb!r@jqQ_Yw{`K{7JL zHXcWS5eCnN=YV;E6AFt|#(w}6KNu4ejQHfjX*^+PV_tc*b8c~4N?zGd@A25V(Z<<1 z$U+nfRe`pfw|H0=6d{y3IwN|039GVQ%bR=ag|_BEPmr6px1J3i-OgbiNfb%P% zLBiB?LDM)v_0n!clPc=nN)RD3O-hN&kFW=!1aZ#1kc`BY0%P*aL~pr8;bOftlfut9 zHte*4gF~2;iuevp1QV4!g!Ko1!#Dzhau8yNw1aCy3LI2ks6-YYNhceCz?RsiX`7XT zte)(X4_bM-N6bX5qNvvT&kH?k%-eNs-b3Xu@LG#TY{J;58<~!EO~%$fc1AF-Qayuv ze*tHo21|ztlf(8iQK5@;8EYlQKDd|%Z3qJ2>)gyj3%%cftzY4h^yez;WGBD+O~@h7 zB&+@O3LHw)xDW+t#iq!Q44L2Z9ItO5X2_MdXxiH=1*4tm|p8QxXesaVPPA-ze?*1(_CbEcxSJ7?C>MpBvW0%PZpSc5r-?9UjraXQA zBf`12{iEzbOx0c6+1}k-8vV5U`L8h_1~=+A4k{zTF}N|zLNwENT)isAEUC*2Z-y>& zY8fQU(t;DDMd~#U+mf#u&c@*&%fT?)r1l_~7^?U!aj(SbFeD5-7APDxR2qg4whTZ7 z(MGWf6GAiL7u48VTAv*p39cMrEodmXZV)1jESK_*3{gH{E1QpC>v4imVbZ6~U#c!# zsKF5mKEStPL!TGRR%dAi3+$Ztg(ZmE|k zDu_6GJBXLz>~tgW0-I=iyUh6tKr$WvZ6lq*FEjORrrhM%Y)S~yj*+%!Al&=9T%Tya zktDw+ZSj%$Rc0r-G%W|xsualie}p7~1{jmu1f$_}d)JaIQ~sABk-ILZfY9vC-A6P=ch&j1NT+TG>dx&gO^`GrV8g0}*lh1|#eVoVx;39(}$5LFEg zYd^nu8vxhF$??Y4meOc4)rAZPQVjbIQbU>Mv^0xPs*Z09V)=Iblk#qGBLro9+~61`@K7$;1`7UDOv%DB1kG;yuif3j5oW%TG(f={>qS0c7OWo6tRbu< z6m{XWh3Mf67N5>O@pie0!x^r80|xfun?>%T5i9!mPjuD8wL|cAEMt*Sd3)F^p8>Oi z^8X;ykXj0y>*pLa>^(P9OiTc0C@;|7+0oOUZvWI{{1Ck?L3z;`Q5a|V5FOd*<@=HobhVaOLG$R_6DrBg> z@JkGb3Zd|A1>2~7RH@W zCf8Q8Z}G%udoxH$RE$z+@M<`nGtaJ5ricBO{Bi1egq}z@p#+iY{qT z1lU+w&Op8J!>{+|ZUOp%FW7S`(jZCaCT0#Dfs{IEE_MRbCBnaUtci36r!Wx>E} zjl2t@VX_!7fe;_@O#4d86fj9<@?Qk>D(Ej@JjiT^8)ka;N&8_^!3%;{Kvue7U)!({ z`u_d5M1g}JL82)y5G{}xZ1ZgtYqP9y5Yvo|t|268K|To4`rL~|&qeoDMv6)F zka*E85lYqNVz7s~yaYAR`hP;HJb6KMM_}3p+?3&SM&C-d@(uo-{+^VxbB}#Omlp>1 z5p4is3%gzST11;&bsSj1%{lRUcH{;zF(-wOQryL*r4v@c)8wyhzFDl+_xxA6Zpm$h}$Pnm*e9J+!#&5U;0=!8v;CIBoljcwm z%M?t-0zMb_?w%^{(=QmL(aWD8^1v()r3y6sEnmW-&kevE*IC_S3Ns0D2Y(rXsuPG< zDXhIoRVo}1(h~F%Fe71OPJe5JJ&Rp_q~=)>Bj$$f@LDULYF&l@~h&bA=viW3I z@{lxHaRxnpFowwi{{#;yDsUK9_mU%aByUP)+I(?7aF7RyjH-dwvK+z;E!Rq2PlMvd z8jxqT1SxN5DK9F&2)bMI1`eRq*8v_uSx92jAN;dr`^Sa;IK?L*G3V8(Lk^PMELuAC zllpoRNJAa)52T3gSiAP*n@88zn>=tfZT;s*8>|IwMZ%cFLbSWXC5kd>=EDxV?}-Xf zJc|l8z{K_`)23UT-7M+=NjbCzL$@BDcO;sPCO)RySE-`@2_?7|U|@-0ht*&z6?!m_ zT}f0RGXg0zohAsI2;XS{xEtZmkE#|d42AKg1v6o*;#)c}JjPn_GYuW3!Fp5;i8-PG z4)g|)$;TOwBXJnrW(x$t9}n%Rws{c#94^ME(JFd!Hfb-u4e_D$QSwz_2S_y%++Q## zjb`kiV1x;DNS#Q`5RFrom6A7+rXUfD13!&0 z2X^Zj??Zx_015!qGK77HgL#j;hn~SJ!KyD{2xI0%m0}IB;0#XjELpVW1uCe;GM3#U za>IN!ByIZa%OVSZV+G47?%AdYLqg+Vio;gf!OX{xd`JRgpA^0YPGWVsk&54w;%Gz$ z>l?h!Uhc|y6XfU3b@m(FFgA{9I8E7IkdbQ-OLHKu09dg23UDX*O}Jod3)IKJo5`I2 z`x5qU8i!nQTwY)O2{X-X4jnq*f{;SFD|d7)aukWwgiAB6@cW3t_P~^=;TKW^4zb>S zFy3q_N8X7Kft*}lf$&b?uP!fU>Jd%t1psI*0m4x|`f18kN&u2fC;`6K1v$1r&>ejv zp(sa^nGk~N84>bQG;t2*iT(oWflL-1{0c7m4r}W!;=#jft~jak z@%ejUc#`Y)qKKh_o>f^s?yo6xMya@5OwME85Md9z^>DM+h?UCvl%7(*5$SvO_wZGs zO|jUo9M$V*^QR!*kr$mWcsm?b1U$P(o|E#D1kz9(#rOmQX2f1s>ayiU1+HJ_r{kr{ zX;>Vj*c36(RbU3-Ie8MF#xS>*XQ!t_104DqS4xqH;B}_V;(K-K^d~K)m79&sAbH(1_fk)>YI~#J|43>|diZ`8{mC(;xAawq&&^UYS)0k)PmX@K^ zof1?Sc9?N6kT%r$+*(an1Tu+g{a~AaR@%H$vs(IbHYiv(JNZw2@sk>~eV{O4DQ(vQ zgF&IvNbaiEk~BO5c1{SNavTU{7fiv2q?+JDm2@LRU2_TO9(dZt9Gs6n`)L2ST@&r) z6IU=;Jlk3E{{As=^MNHC+6L?6_A6|?U|6%Dt z_ED`2&-uWoI0nce&JZ(`OKl)bU>DU;i$sFVbEXhdp5UT39%z6zY5rF69OE`q@+Q>r z09unllf{F%g9N$r?jo#(>7!AxlZ{{BXq-G2fDQJi29XLh-3pwd)Zs-EW=ADc8N-c< zX*KO@M_*~2>gXV3=~Zf@ln5w0<1}jkS%hBVL~`qB9QE8AAka8>L_FGev86Uqo~7Dl9*z}DqbfxRx52kC!8G-PlH zt6+)^MK(-@2%Hk!CHY5sKvD>I_f+~JnUx@I^pbXyWZ<6|i4aaDFyja~OXX03!#x#OxWH4O)hqWp9hrWvYGs$78nQ%E-TU_iPn%15m`%Dj`H>HU#>cr>ut0FiV+` ztxB2XQ$8QNl$Cg!PkYwaInrT{*p6OMw0u7X=sk+S1|(uQ5k#tS?p&%e^$NiBQoj~# zsP-OyV@9J#tsfwHN-=P-{uIEm7Mk`<6QFcuh%d_9foL5pl5 zo9vbmi86OEBL189=vY30-CY23ilQ<~*yNB8W#~kpoZ!cr%xS zWX(hH-C~-N6@fQJj@p6=iRgF;dxFp2CWKkgP)ArUraA>uM)Ar|wi|zi4|QtHEhP)a zi~&|J z^7FnY?M|%7hCd>+v2o;r4{Q02%9EEzDdzA+bIw#~)!2CBm=dX6Z-aVGqsP=(CgVOkhWl>_8L|MRKz z&f$iHY2aV~yWbDSGE@UYUts!2(YAr385lq)5VWyJbd^Djxkcu)!^h5n;YhjFrVbIJ z5r+HB%xvA=FV=4mZhyU@Rn9d9$nM%1G?9iY9#W45uwWS4u7;uOB z8=J|zi-US|U6wdT_^av4K#SonXq>X(>EFT4L1-Y8x5J;cAmat^4ZWahVgZhFf(UfL z4c{at9NCe%7jY-&F~KIUOe=RH2tP{^1eJt@xslYtbddnstc9}>rdXo-4Y^uE9!x~o zLgz;D;vp1gqH4z$0tzW40cD|-6|WDTK}Zm8n8FPL&jc5kuoYMj5jkooKod~e7LNm> zfFa{aA#4REP-%>HzaX9hy#+T_G7pwl#dA!nDl8f4>zE>eHt~fio9d`>G z2gnzC5wlA$LBt^~-hb2kPAGI@`-;9P72p5279;1h(v4mp(~bD+j>JIK;6^RqN$oPM zT&Z#Q)tRhI`w~MchkT|rWC|4lEfDfH_0FU=brMJPIk-G|8TkF!YdAy%(8boz5%7Zm zbM_mprG0AHil-Q1h*}XGk`2O6xQZK?;f*MDGsGyNO}(BztL4rU(3C>`RDJ|o`;Yw#2I6L>-%E8wS% z1^vGi20aQBPkpguDgxtn`D^wi7OSCSfk|0oMDSK!tQDo=6J?o|QK%W~q4fcfW|qs3 zfA{Fh;I%0og1T>nkDspYHT?`{Uu}Z|Kc;p9lNJywQmP4&6RJn1OD7;%Z7e&vjNmXL zeg-mw41`S536|8L{*@`rrXoqk9n%nr51B4VvbA|JuEPHc47P4tNhD zOS<{?G&_G56b;0WuweTh`!_S0dxCfdKu=!}7RYHVm6V27dmBF^QjmC|_a!l49;8?AqX(pP3(kkM9{rRJKb zVAWCh8Q?hZ2-q3qFXT%Iz#g1IX$(pgwK9hHK$E#|ods1(KMW2GM zGVhp0rXHuME+Myz=GzUfSitHT>nq0=%GWq|Dz5`9tFuQuyU5Ht1k+vqE;yKc6$Yuk z{%f1rql=nw2TMcE__ES(q= zQMB*T{rod24{{631^4eaP5kg0a!G6rQXu%1J}MXkTY|%SegTIF0mJaXrN;gSk1*K`3g+K}-H2zvoYXu$8E~~x4Z^D5X?w`- zeEP?A$X6xk$uO}lvOCD(*re4^{r*)SYg+Yf@tBYZNb#G<%et0nc=fWn0LjN4oP#2- z%B*g}bYsA2c*LS0afoSy-=q(yG0Zw0Q)EgTpcCU1dRJQoVZ19cN@QMyR~dAdD3w3N z$X4jTGWb}=80vC!$g1I_5Q`2j%{V zdJSP2bp^NHj5sxgTP5eHRJGD~?VDGH*VEIF69*Z3?yI~>p$heR>0qhzj` z&`8$FAFEMf3>)r@awzyXT1K)8i7B1@*w_tqIkif$0|E;bsdJ#qYe1ovzI5EdF(6W}Q@$jUvW8fqbM$fE z4qYySHv9;J;fxGmcPh2Pv-pnW+M@gr)gLsE6aooM4Ym}Pv2n!8-%6{?_rOF;e>KjCXa@UTw;$c4h?k{X*bl{_lH#?g7zNGxzYW19G4 zY}KrjJ#Nb+qCYA9GHjYYKf#cj+Aj>k^*O@Cnu8NQ5nMD=GV zc0p2@rqmgr)wXp9Oczn3KPy!nRY7G$N-)(=`wkqy zu!Y7!a?`v+rj`VzN(_V}iW~+v!W1^6*1CONq#i~=FA0DtVXCon;F3+jNmc#KmJuKA z;}h_p?YdqkT6G?j*f{KzuG`tTbLcyGsV{m;(WO94V3xv)3rOxi>jc=BH=?4=fV)l45Js#&^t`CuCc6OKBPXily>dH)sx$!x_l-g=5fqSu`8a! zfofyyQ*sJ{?rQEK+{DCjo-hI7lPw`(0uliGM~4obU{<){pRQ%d`fHSs7xV6(%mqnj zn3*yyd_RgarlEuacY+|_*XAOK=4TEn*#~r8C>koM*_PhxCWA>c2yWs@LjIgh3w0XD zJKYiT!Gl7mVN4@v@U_hO&qon#V|N132YN(T&EfzZqmbY|oa@1=iEhCDtk7(^!#X^O&#TZ}{QUTmBMxrN_Lf_O$bp59mRd z;!#~0YiVufcbG3(%PGku$m2`N=kL1aKNOcJjEHR(8kA!PMV+u09mJE}WQu=#S_Pj?3C6EBFjxkC7Be;z?V9@gQC602 zbjIv53LcZM{$;RW+L2^7U%JuBE`c0(mFPJY$`;W%4I83MXVeNzeiI*Gp}gm0TRy!7Umw3&aMyqYnotwhI?m)SJI$C z(5I1+s2?W-SjP7K4Iu?>DRS2ncl#pOY#EKOIu5X0qFHFus8D`~pT@F)_8<7EILEbe za%c>p?O0ocv_bqFdR)6p*W$MXd30G&%@yy)1`TW;)i`86!SMLrP%D;?e_l_-Kk>(AA?EZTn_(5Q^s-)lRr zF(Q>PXu$i%wT2B4Gv<%K2Fu>kk()>7t9WIo6{-|r?8XJNv8F{DeiJb{KP3vax%iy_ z_a7*>zR<8CA|m4Hfv{SAvDICOosIAx0{X-H7pkEn|}mT|U_HrfPsD@fBa zt>$5kgQ!DVjf1L>YoXKQ=FGAO?gl$|5+Op}o-*gP*RNmSej@w#!RxA)+4UnRr&0}$ z6hP0w6cj!C-xw^z{CCnIHlMcroLYgN6ED=0T#QPj>yU z@^CUl;m1jUVcI9TzR{aGZtBv8)q#SuNtP7Da&Q-rS^62OVaCC+s9MWKyjILAx?EeR zPa_TW>`$NGPA|05X;R^gIffNDh7LIr6*251$!)ymIJc(7Im~eer_KgvIS6cBR$gA? zpn^JUhrY*$vJNsO{(XIJ4c>kFWNsi4uq60s!I9K5fPc_`*swZ+`ADFZ1o@3wHF&lz zd+^{vby}%Lc}ZVMP0eDjjGE{~eA<>^G|81K!BcB7!&KatZHyzGx( zQBO(+$GyhsaaO&Pb7oP2V8-LTg$W|Ck{Pw-qNMO=9Xl)BH_(mXT5K`yj)6Eon&MCmkH|_jO&K=fG>LL>)nJ}QeTQC%_kJlBevp!at1Geg=V!{t{}WNh z>=o-V8|fyy#V4jyu?D1ZBd4VAiX)2NGqNtTSUs3rky_*MEk{XTILDm@!;^l&aYk*O z%~B5Rv4cuof0_18_ahts8Z=0Tq5p%L4YTVLJ4iH}Y_&#f23o4(G<{ajrJH1L+BEfq zV1I*}^a--p`SQ3Kd@4!2)P7veHld3oqIE?oeHv~jP86QZ0TpmKndpq(6r@#K(10ar zHu_H$`86COBkOQtB=VK7VJD!%l)R;7PQ-B>Jb3V^rzgigpeVSYmDloHCAr!eo@+Q71uA)SP}-=GW}H0aBgNfVJEp?wnP01Mjpam7SlyyP9RqgB zF141a=4mxRH#$v^4@Fx!DCt;2YH`lg`;a^FBPFfg>F0U>JYw|TO28N}|Kjo++mB(h zjd&Jm$puJfOl&L;7$0*!I+h?z4I9of!)Dq!O~I@IsWZ84dUYx8-*P~1SL)8-=|8+Y z9Hr~1J-HOa*8^X^uwZ~6A3_U6GD()|wr5U@*nAhETaiC)dX8O`$3XjH)YosU6^m+( z^o)b2oKY(ldJcXi^)Wm{Zf*-!Q<(EU4E0*38!_Gk78#|)^w-Drb#>_a@Cl;fBrh%V zsVyvs4i;)Qx5lO%w$U!Rr~muz7Ekm5boh+2_;-9j9<>+P!%Rbn*w~`#?bDnj0RNFy0Q!*5WKyZtloScTd{JOe&C}}{|DK9NI z+t1;&*R*Jcs$#T|1_myrnWiuU4rB)$@TS08buU%U!rWH3q&oC%eN^hK4dc|QRIapW z(IVi-MIHbCdkN;u#MmAlt>DZ*@tB?eJqO?gL$i8isrovI+wZ zc!8AMk<}PTIfJRk!#UGQAbiUIzjm4$au{nH ztA!OwL*K<0fRf53^Rk{iv3v-1Yv0PZ84~{OSRi*fJkJ@@A6*%^f#j1kpos|}BCrjysx%#n&?pUf5}?15y}SVIWaH}Q_F|rI_jFpjiPNAb+$l7es&5~l z4H2Ei5qne`=V)yI^hf1^l0H5AfSk%DI9r@1B)`TcxPWX&D&Usm@*P6+QcM5xj9cvB zhJM)ic%}XZXs6tku2_q#gO<^iOW+P2@pO);vont_3N6rf=o1aiybN;yNYMH^JWM0b86NENbn;pn4zYzv6zaA-4i`p6Gh}5O;MVqT#AOIbck7H+d6a`!Fl4OpyetkgezCc@D$*PAwbr+00?Xm zJxW-PfqB@1Ko%fMIuJny?EEuGdvbaqx`JSq2I#0@lqCTsv{|W`kpd?O+=ILmWXsr| zj4IS)F3avc-iKdLbU;rP`n`qm2Pdt5+!z5EJC0U zUaiA@tr}hfSExJ$??)9J|IhCd)a9Xb%8OC`#z9k4rbU-0MvHhtzo-_t7ZD5Iui@jz z1+2JAjPGKN{5V=BW%&CNy9ZZ5M^(*Oa3lUjfGq zzncz}i9jX;KImjliwgN|vQdu$SaVVP6zb8I{8)A4#;>D|lbSTYglRHm&WIRA=s4pe z5F6oZ-*cF4s`snxjqL##9y0fT!7d7?4Z#WwDX>ZVyf()jd5Q8B6pvQ7tX(E7sJCj> zn0vkn!iEavj#FhDr33NvwR_Ecj#qiDLP*P?aT|nyJ57Bf<0r>j`^$C-#`>G@B#3Mq zy%88KqyU2PR#{)K;v500A{7cE$&+4y_$up@j*gAzO%B5GP(c)f-Dp%5V+4sQ$fF224tRFrT_a6ufv zq-dyz0O`UEJXF>jjdDuPzzIg!MP=@UX`%pukqYe`_ZCmUXA`^*owwD)QY%siXcOb& z&r%99TgTEl&h`Ra3Je{r8#!B86{^mHoe3^5A^fbJu^3}2skm`xoW^G&1`t&IydOx` zPoPTwp+S{^PoI*s#$_my!uoiD(i7{!v=4Vqw*MzgBPsd2t)3 zs~1^6JCU70;T}*@DNw2x-U#mEopG}!>f2xsn9%JaeU{Bak0EjZ7q;bbJ?@j8jbA_R zH9JJ?3Di3IDtQzdHO0i`{`^!@z~FMKheb09@F~|3*oRyyK95(1r@tfg<=)5Hwe2P* zCN93dWmaSyJ2Cl~AgBVwj~HTd=%Td(8N=}?j(=Dx0ky!9!2J9)Z3xGTV2>b4jW2GPEtSN-2j>NIIZ2 z_a0P~jYrm)njTxrsD_kM(p{EQ&LWf=Z4N1iDN_H}^}X%+{~Ye_?(V+#_j4WI*ZaCY zpU?G)diDH$1kv^^w!Lz7>%E<=SF_f+{GD4#VP1(F3TG$NqCZ1>b?ocqhp(9cO?4BY z&U`+&LF$1Yp0sM3gjVw3j5KxjLKh z4R|QQ1JNt&z%ssCp-6#{&q#^HhtAmu39TXF5!d*{C^!EcsfnWWmpSK*%j{A&4NM{h zqX5JE4jmdpxXEpG92hEH5hrSsnEkX0acbMt@ip5e{&RM|KqylY^5uV)uFVa87aN!L z1cXQ2XA&O5n18rts!dc(s(~Yz`Xa?94Uc!vPmfGW&@1}YXJvc2S@adV`GXZfNHOY1 zr`Y!BQ-D&CbWaI?B^+52qc4W9O{u`_VmD>6yH+ea=s+msDzaz)|r01l=J@F3nqq9%P&;00@Q$^QtIvL9>`C=w`!4ykYoOx3*~cs_Y@*-=AE&)ltI1My`wz!EYL5Pfum4(-7&NBChd2^jE^O6bjG&UhfyJE%dB= z9YUc86ryr~Lw@zYIz?z`C`nf}b|S)gW^#Ge)X66_9x7!`=kU$fQ+_OF=_?3FD!b-M zu-&fP3lHVrHShi&kq+M3;r4!E)SA<0C=y@U-hIugB!}+v$$yvJ8+U&-6*F=kA*s2z zIHL?y5+N*4=$cK{!tVxejQy*!y|-jUK)xbx>SdDgzpZG>PN1(-+FDaP9JB&v5DF9% z6&;hIiX+Y^Obssj|w082R}8I1vabXe-02_3P-q)8+oVEf|l@QUB; z05v*WihpgSct-t|<|&di3Cz5e?BBdc?rVTW3)>-DHy`a#3pR#_laAtGXi~fcGD78( z8woicDUk-cFwh1L;M(+cT>qh0{?wJioRnSX|8bm}*vW_n1eyK)u5%kS`6XP z(Yaaqd3pXM82|g_1*T8Aw?~Qr(x|ki6Z>rT&n0 zUB1B071?}+t4RI;HAU94$Ak*kX<1i)?p#;JnIB0t8(Apan^&l5^GH75g4;!Pym=|^ zC(2^*Rl#;2Q|VD`>L_CQZWJpXZ z9==A+_>q()3eGL8-|E!-^fIh~blZg1m3iEx;}8~0%FyL&!VEgj$~b2+u-K(N(EH| z4Y5S&10sDFwILT@V48r57?wMHjm;}HH(*5QX?XaCkpI<0$bTknLn&Im_w6-P98Jl{ zMy|p9{f|BbAeJ2Hlc2ZeNgD~*nTE>LXcw{1kY<#tCux%_W9U)b3-$6?e|cmt6QJ2=YJ+UcKZmo<Onq}BwK=Q(YOz>55(ae%h7j^NDRPxkCqtP*$LG>Oin1pOYxLZ0 zEqiYeh1r^gr*sVe_1pld+;`j<7#c(|8(&e1B?dwx$q0fEF$pU+a%Y0{FFD=s&BcBf zzX<#vdHFx$Riy0q=E3Z?mQzkr{G%vM%#|`WtWhGzR_*=_iVVU-&&~YlbcKF*rkJJ~ z;w{QNUw?=Hq@X$$X1!Aad2#oTjERw6LLyEFhaYNIMT{FaS?AQW?uG3)mvYR5!@~wM z3TX9|j$TUQF65}%^5$*QDX9U$&fkSYt~RK?pBd`Y^pG(3<{chrWQy!gr zc2-Cuvlvcoci(HJKOQrp+^5WqN|z>ZMO#eP8aPNY(O(kzQv3ng!p3nN!W1R0$}!u% zZAc+%0U}=yL?~1TNYq!Jb*z-BXVa2(h~`)6ua{hs2(wdauj}3!fAR8p_g@$A27uCXJfJ zWA9R@WcCQBKORY#I54)`k!uW%npw`dC#T^d5<@|Y{$vdj#@6{sAppbMLc$~Al_%!( zD8IFcP!PCllCZ#XyI3SNb))%{`yxNqa7b>d zDtXpGk@d-!zngbIHRx-&&`44<`jFF<>FJQ)p)hjzzy&!b1UGK>9_B@Cur9QRneA=F{Oe z97`7cSMTe){8)6sY6VtBK#&0NzzJX0LSd>yNEyZ`OB{>`**!K!;c>Nj?+z>c?@lJb zs-#pINFG92nFNit=9;oHU1b7`DORGJ)Mx{xAbVgeIlRm{NOOjomhy4zuV5(J`D^V| zI89Fcv;K1u!XS%bS|1j-FO`eJT)rM?4g~`%;zHIu(S}rJGa(5dF)dPV$8O01GFv58 z9w|Xo2{%~;N&pE z3}x`PKir(^s4Gz?k?5&~n*UG76>`qp?(ajKTHh5v2KM^LQ&jzWproq3Wif#`xKP`3 z&=--`A?YN&FzaQ~`STf{S zyQR3?T3IX-b!X!JLl0+|{C7;FMf~EradX#zni>vfk)V&8k&ayWBAo~-n8Nghs6d1= zI9^!L0QjP0asuu02$kINsZt@x&(8;#p3Z2maJuW)ekQhpLfT?VuObvlQO00cN*yL) zQZ8#NB!qEUEWOp2_$e+ymA3iyRBD$>(K&Bx2vv%a-^s+1vr#FzVlMR%fDk9p8v6YU z7z@+E(z}sf_x1Qtb1r*Dr^aKysexjRRqfMyKI|X+p6t~?QDJD-Y zxU;xz!Q3l5MWN51_gK^L?H5C&?9Akga4>;PMTLxK5{wG>?_7=5!G%6}TOBBuV!#Ug zNaB*nS-abhloR3!Q;}FI?fPV{P$sK>nvp6CF{H*hQJLVVL~qE190tQD*<8H`7ws%0 zee0n3z~Ea8ArjqkXakI@@ZjmXKwXcfvzbd}-an`e2my6e4 z_4a&k^VLV&zPr8m_5r8q!7j-Q1_rL3pl8r>;^US7nXzle*I(&4C#^J^;GCPC{@bt9 z+-(bfv)(qt^Z1ivZc7L2_J|u?a4)FR|Nb%To>r#ZTa2oN#%29t+id)1PtM++a%+f1 z5DQ-H9UM5?ta)c_>rPODehM^Wn`Z(+Ht0l${-v_dv1heG>6fen-NgpR^CY`khCF)v z_o7{2<+8-bYCeX^fE7FA(kiC0$fIJ~T(*}ue9^}vgKb<3S9<@$=H2s^sbfBWR=#UF z0gP4m=cu=@O+IrkuEYCYx4tj;e^^WzE-#PSip2wMQl?Rvacg56vJH)rBU{^^k2ybU z3Q)?`F27`P*Wm8Go;fUvnL$7+oVM^11sDwdHBA_y6F4$JaBGm_{M8of7-1bGK5Y4k z*MmODTq@0D*lr;+Ldq&bTg)(t4J7P4Jx%DLDiSEP!_c|V1l`Ga3GR&9nhStn~-O5wq? z^EoKWE%vz^TYg(BW6uZM)kCJ=Zl0y>;mZ$XtE`x|oC~mR=sr4Pvi0QCf5kQ%$Ck*N zJHjF#r7Q(M_>kp>)r_TL+QjyjK*A08%uRBH*k2SuMsKHE>{*Md;BA8~sJ+Y|%o2!0 zi-trum2`=Ibt7v|pJIC0`g5xJ(lQzofTO=nE4tKa$Ane4vfddN&DxkxrxGVwcpKh# z`S-hhm+3gkoZoRns-V)X}&mSCFA&OO` zxgB6-@s)Htr@iST<;{;C9!uiRk0lMZh3g_lKuRyAVP(@Jugv$%4r=rE)JCEkYAfDZIGa zH>Dy5+5GOQHNDZ>C2Jtp=Dlb|%0b*E0h;C1_{4Q4^tephfYIAuPuB5s12=CC8nZfu zA}-5pj-G1>BbCk@!EY5!k%+pQkx2BF7M>uYZLZ>-x zZo85CrZ;WEef+ZQKX{{7bo%$hTN#~YO9@*%v?T8{}oHg}#jIN0vp zRkU}cWWVC-=YObbI-0t)RrH|9C<^MV7;K7P%$C5he zAvTAm7#C&j^1_unsrTf=uoT-<+)ByPP%o#+u*_G{bj5u3?y<+26O<^!Nrorr+*t7B z=^qy@xp9s!2@Ng5ovBulFfkb-?PF;~Pv=M3wiUCD>!B}MnzNF{3&kC;i=WZPMEZl$(Q-BqA2S6$sXR$MS7Zpny!9Da+*)nYGcBI@$3a9cRdp3K;>y7s} z=NK!?!F_SqivI?N9g^wOLxgPTOxxskhx0B|<~#KZ>< z_9Gl|>?CJbStC%uJRpZt8Ni1z`6Kve5w+v&@8L*syRrswd&;BDdU!pd_y<9lsSAG{ zIpzi{+6=KAG&5Bz`IL$?BJ+h$hp9^TQ{1nJkr2!-5g>|>(w0cM3()x+t{W*o;LH`- za5(jSnMU^?ozhfSrvKeM6UGN{C=KmL8C0>YVpPgBV1rGtY<8q$s`f}w+JDO$I8vOb zmZkwRy^thRujF*3-gNeDW#L3C%;=1i{%PrL0mx}h2iw#J61JnVU#jRy#WccWj0Dih zI#^J_WE~rQS`u9PB=YFWA3Qy(iq&3GEj!wDoaR6P;mo1aygPhtfv)9Ob$nKa{-Qtd??$#ss96FXa za9qXU?GFoo94>7DD@IpweFq|tKCymK_Go(VMt`Q5 z(`we%$>bgENR|aF%MbnhWY0LRqG(gtp&TrSEWe*lGctPJpe9a4!O!r!Yp2NtV`}i` zZ#3tX4L7ugj-v5q3!6Xl^7HS~?p5qZIXy%cTTqcfN-#(zZL}Dp7|ddrC{O;jAHE$j z=0+rAgh`XD@YSamjjUJ?H8rNN1!xNyUTfWLuTxe zLqRzSO>0GR+z}n1*@TobjmUE`h*mb+H503E<=$S#a6kPQ-4SS$fL&3=gbWuJ&Sw!d zOX0Gv14#6-F1Q^)-*6JQsqcd@vbLDbjj7F?UL!dPCMCVN0Z9-=!2yZsUmOsUPpx9) z7#dGtK{7K1O~sf1p_hUa1zb$vRADr&KQ0&y^ri~7v=>000o;X8Hzn{ssI)k`pVoS@d`t+8k2FcEYq+F!tZ-(H4WNbWH3f0jZd!csV}g1)}2_?9(kqYRYJ zWhDB2r#u?IPflmR>!h%meNGPEsMPL`pr%_?BuhXD=ww^P9LD1C!3VU-msP!Qw=mLI zfavar3*d}wb(}yL<47xZZ1>)3e!Sw@X7{NnO)Y;`Llo?`n)f?|Pj{>Pa+xE!vPk%- zH;19pZZwotCb3h}R&Gjqef4Z%lHQ`hL@+S%*Fc$+!%_&QpZ-fBTlTvv9VV_j+b{OX zTA#V?{n}o=g~S^>{hIDKcc1u3edekJ6@mnrgwppwPDG?d21u)LHUj3~ZS;st&Gn~b z0iG}!w%@b8PL@>c__T^Ir0RXA+_E$1#QZC5cBTz>T=P6m+Ma4*hFc`LrKShoh=tI7 zjpI--Ln`dQvmw8Y{_kC5J6^_CLPI8|K3m@L*K)LuPAQ1gP9(a>DpXeH$=Oa50BJTv zD71z!bM|*@3A#lsw*&RM^ou^h)Bs)}%Ziwxak@*w!kwQ3)eqemWzgx!EViU`?$C-AA5Ny% zp)|DzdVw3o59sGdn&>h0uHBUXd`j_6PRYzZS457wYCI<#-H?jSeIh!g?}@FycVGAB zp&x7v8zSjrC3yw?wucDTe2)_g*c?m3ug9f~Nqz(mRjk>K5uu-@z~uq;jcE2fe)QRtH9XtM5VTyd+Ca$@=r zGg^>d!~>e|zPIi~7glz$z5^8=oHg9qudtT=AL*@-A{LdX*ocDV z%qswRL-(zCyD(I0!tKrdI=+D^6XqbfQhOrIhNMWiumz&ESqe$EWJ49xir&|fk_shP zI{UklrD6bZWe%s215qlJ7$)zgRT|%&R(^-8J@U5d1$h+yygCK@Qi8I)C^Pvol?!aj zOeCAP$H+=sIK0#&7BCCM=7o*_loo5;y%+^-uC#O#(m;ot6G27Hq_dpl>1OmAQS7A{ z!*{Y|k_I#EtFBro5nRq+C6(JMaE5eb0-t54=z&BrHRiA0`+6u2Ng7AHE}*ujQD&Dl;(*X!ZnY zw!-rK%8s@f!boMDLAFYpuo$Jq5SW0ZKdY4`1w4No;SQvIOuJd7buH6bl{D%d*!+nM zEr)s@B~>a>Aqwu&u|qj(Nd`znWsXQ@Y;Od^`D7NF5J3QzBPcj1Z3{gqo^cnI|M`>< zahHNK3nZy^aO?pYK8erwT1a4qNMUfS(^ja{+l;r^5@IoWdk2d$No61;`R@F<&FOtu zD9%rely%z?XcBLD4-2BHxtEoE64lvH|3yetj;oUGPIv~DN@ULxM*#MmLL62?voS>C zXdKl9a!H^~I)QCqT@!#9!E(u=tIhK&l#AuJ`&o&dze;B#CxlWfY)6%IGV?X=*@SYzlQJ{C#KUhD_sXH-FO*MT{Z6Uo-%Xyu^YBp@3;!o$k|Ld>1;?dS*xoLd-GLZelwQ^->FmU|;( zE1ce3b^-!#99lGgFx5?|M~SM4>pozFXbF7*!eVB zDbVugjM&qXWO43sY}n0$OLO=#I#E~lx&B={}fE30~M$__oo9v{!_G1RvQYFiI@LnY+SJ<(O0;tm^!8}L(k!oQK{|VFI;k6VIIJ`pYOmhcG zz$L{}G)@T{ddUSFq3BzB*iJ#q|HNhvzYgql3%0;Fm}{e%w&!NgP~ z_KUQK4{q+Pztd9bP?Bh_HuE@z1gmpO8 z@Sb^{rIg^OdvN`pEV2m>H98e2$9Nh^`wQ9T30)L?rS`qOLI=y^sPGy5a9|Glw8ar0 zGWj^y)ri)dJZM!Y$DBSQP@h(q2&GdZG)=kL+;@GclHk5A3Y5|KDsUHz7znmXUjf07 zlnNBf?#DX$52KMqYH}Y8fP*%ltmOiD4@6#XJYi))S8#d;gPb3RQW}rl-g8@8-&ehADt#IeA=-9NX}GJ6H(gVMG<)- z9gUbnlF4aODLJB)goQk18fSmsY>^RI(x_G%D)j&!*0kCjS&v3HuZfWL~3}z69hb zG6K#TQ}Dcbm>3CbUucZSXA(My8gM{4NmGtjTeeIXheAsL4`p~oPq?P2h?pP~D9RxQ zJcpf?j$)@Jp;$Jh5$(vKQ>Fp$ze{JC*1PY;{zffjm7Er2XSw*?s zgxuCAUu!tj_%#R@JG465H+OYULAz~L3Z%+;mi7G52W%IjE{w9$ zsbjaS36{zQbA{AUq%bIl_R7c6W4YGh)X}FiP$p%l}ox-GY zmb=jeWn&R|y{$@GbwUqGU$D)>sAkexrhT;5A}bw*$&c)`qSNP%7ecq5T{Za^Uj=3}QAw8- zB`Gaip!noM6RT-Xfw|7JHDw!*>@Swv{|M{GD1@!ZU6S%jS9B<44jUY$?<``3qFTn! zhG8z_7UKKDo*9?){}a-woK$=R5i1cI03}yBu%oX4sx*aSB{K<_FrQCoFeJ&@6x*7P z_LtIW5$1G}1KleA-5f=$433Y`Iu!;wb1E~=qo^7vFiOu#?L_RLF>k8~Bc7b72$Cb0GSq#E=wECndMf6z_6-C0NjXk!eqinfUCavsH|h6-LUiM{d8{a^^FH zMe0l^ID_@P{0gUbh!a9_|6G-z^|ozMy{jK-$;u;{GRZkdWRsZ)e|b1TERl%MI=Uxo zg)S&(eJbZx%7oqDSV9dz9Q5c_n$BL}iFl+E5ip1-$Av^8^R1*o*cL`6AWx3P5kL>e z!;ux=fv5=fu}47UC}DCgfsUPe(mS3DqIb*qomeePzw8v=#WN8dmOmFdhCxGJ6@#b? z7d8g}0Vngf0>whXAZ;-+*ZtQG&Z<*lJy#SMJUz-_{$On_RBFxwi%L0BNp6sbj6}}* z5R($aRBV0$?fs=pFHP2DYF2yn8H^n?AkY)II1mrIk?5+loW`2o-++&hOmSn?k1%f5 zz9wn;1aM1k_O{|kaAHvj-7Xnu6?}qLC8?=l>())*o?=#OIx6yys3Tpo@5%9jk^{@) zP{-&NLkOa9dAU!Fk(sSem);{h!bP+dP^@lLY9;0;$j9Q2s)c^@3iGpuy zVw}VwN`(v%4yB?h$1YmBON>WS+%ASLa1;dZ`bZ)ukCH&;WNBx?;+`O+av4%*R_n# zI`fu)Bgof+2Yssxw}cQEWgkw`TP`;eU?V~!O%#XM_f=ZvNwiZEc9E@uN~CUYXJ9c0 znRsU%Iy2E62Ls-)^ofs<1DHozN*8K{*YgFX%XUzu%e|DGL3!{VdZ4(wQi6-qd&_4e z3kQBBL68my%#+``bxZOnY6n8psL}%;^vzrNlUU!`-`V#`{jk~3?nGDr#tY3$j8AmD z({5rC%X%!%NN(#QQ^684{6R86jC^w!rXpQR4$MNjU@tDE6r2`N3s5S$fT}VU zrq;FJDtQuFj~2ZzR^6vp5W3n{HAEIPS#OK#t1xU?^8ic^}j!{#Gj1D&VUSROSA0;y|{h3SN+A!_u)3t_ONB1V&6wyO@tAIEdzi zBcZwfebI_Z;2Q3vO~GN27HX}lO6XALr2d|BBWZi<6_@B5g`pvMVJl$sk^>~PvTS7C zi~WAA&21U^u20z3q_vYZ_G5~Bs-FBKkc~vvtp&xeC*YFo%42a0vU)gP$tGBNMlE%S zw6Ksk!!9TYnVi@ro+qS%1CA91F)F{E1KFogO@t)r6ZLC@p0=Bwk}qHj8|Qt9S1I|u ztrEdl9|%-MNJUP`2*FM_qb!Sfr?-`wLV|&1e24GA=1to`D8yK!Q+;a|aYs5`5n~zc zk^_&VRl3aUOV6xjto_B~!Fpv|vqa}d+oh0Y3m6lr$UTFfjYiMRbQE?I2Ugl^(K(O8 z2BBYBM=FlQoghPggJs)YOHWMHGCFLOUDIqTQ_k6y{L|0Cr_=Q>^Cz8pGi=m9WbWv@ z%89SH6u+r1zQ+>XT3OA{+5%`D5+jFAF&5EcT%x)3W)NXSHaa*cuQ2CN2U7_E5%S6Ag;*S-_0oZhER{c^EO0yO!?-K z7#Zcd!wO!Hm`K|PN z^Av~4ny}i9-n>jje6o&qW{6B$D^#8^_r_d3)lF)>()*hN2<2ujPOzim*goU0+J)CI zJ#*~-EWCJaPD{eJp5sTj1((_Q5BX4?q>B31<9vUI^?jUapl%K6GoKpAWz-3c;HpOvZ{K#SPr*7tcV&vyybo+7j zwcF;F@dX1z5AgePS>^gZ?sWs>m)?H-iG1eQjS<@>N4oDVIR1WoV}x9|@csCu+~sGH z?p$)YTzAOr5&TU4vzDF9m~Yb@X^c3)khaUq+XrrAf!WuSji{ra$;#|`c|T-+ zzJui^#}=RebA`QqpKkYe-``!twA9+CzjIxOpX!_cpq1To=H_1Ad08ZTI=&&1uBpsC1ONtist@Ub~|Lt4+Y<^Ix3<-aJeC(yn#6(y9V;)`mt)0Ns$x& zT(*4b-h$hM5bfyl3%8XQv627%l5cLv_Q^(k-ctT=5)B^nHoDILn*Rdx8m4q3_uLet>Rg9Em)(BM znR-K}=Y|{#*;U`1fgT$pF7e@019?he?wL8b;*0{YG=?Ac8d+N{M6IId@C~x_h}E1bSgb_rK7p$4!0$a zS;IxcsdwMEGg$87nR&Wm_vILlW%+rSU)k=a=3NThVjjrXZ8jTiTn%4V9G zF(zHK8leOl%r4*2pGWLtP(<_1+>j#c>fBbl;OhG}3uB!`p*!Y-5FRkZTJNx_*T^M(s zs&8IdoogOw@6e}P{nT!oCb7k|z3!{XnwXb;=U3;3I-0*nUBkZZIep$~p6v>1%-`~j z4PQeyg;d`<9I`8Rfc+9L2Zw%|X?6vgo97g?$TqN0=8nOP{PqQI6wL92&#D zevfj@95%#rU*_)5cVq>e{}Xrn+|&G1&*}Gfo6jrFnkkwb*|aox?*3sz!lu7W30w0S zie!01k;6}!Pdz<%Y|nh^a@OITgXfOSr(af;?d;yCVRvdQ>llk=bDLB7nfm6aIiA&< z`mOV#1Dlt%Rmq1wvgPg0T7I0`XYShew$^^p^!e?W<9U3Oe(}q-#gp!9uTWVN+wo>b z(}SpenMvs8&jz;m#l*X&UknQV!=a%}=IPftHWY#zon~Y{aAHi)xcxQ{&rfQyi(l<8 zUim4;ck~`JxA%7|R``kMJ^f>v&kCEq{Oai1_@DAt?aTDpKP+teIJx~zpYK@i&Dc6V zPHLIr`KFWxHJ2NrHstC!8)}R+m-1wnq@Qdw7-?wGW?I=q<;ZEW<42yY$7^1XmkS_} zm7^Oz7tt*Xa@?P}RoeBno2J%Zx)h{2__%X!=CQVFI6zI>h{V*pzHPl?H$HjQ`Gc5O zS7NPj4@7%;_h+7~KbB{YjfzhWf6qRuV5jGcXATWHnP>TcrJnbGX`JpbC^r(Kz+ zot$?LZR^_JX`-8*D&=eHnFP_vI&V?pzN!|L6Skf8N}E z+0456Q0A)%g(nuq`%V~lq3S|hScpU2pmwL|e|PliS6iE!=J3-7!pVjP?uLlRfRD~Gi}Uqu4LaZ2d!2nl zTPK5YpYPZ%7P7YZ-m{hm z5y#UBj~2Cfx4kpcswCUsOtt}{5S5L#%F%K5ZFQJi`(EgY6DM@7@9*y7bJk(gpyb+< zJ7;>{-`)6`=fQIhzKUIY-kZ+{ij0Y72v||#^vu&oD@G{DU6uy4VEW+O8@+oYC0Bb$t}R>WKK zTj?qt=TTOja>Pg!Q+cHBpIrVd^VNuech#R?J)zqz@7u7>&gj$1*(yih?(lHWo*E-} z!va23o%gBdTt3KX`L|)^)^m4-tr?Mg*5NjTo$8YkX2-4RITws>SOv28y@?3dK35?A zh{K|_A?I&+-kZAiKxW}J5#H2-Pd!5xHyq3KS?r#Ee3oZou20&Ckn2J2fro7ER(X!K z>k}GZzENEK@a^p22;(X%v(9!p2xgL1;WJ+{c^~+E#0g~jasoj zQWd%Rw(iE|zwN#Z&@Rgy7WwnAaYUk5W7qvTqMLa`L)3$*o~yTr;{a8PYL?2)9QkX| zSuudwcF`MCXJ4I>J;%=W=B1#*lN-Axy4#wHEJhg`U`+gTA7v=EZk1!8u};p_{5r?Z zr%v}%PiM=xLCZFu+PtaE`|};cf0j@7n;E_K=1vqJG&}ROx2-zj;O#6`LBzqwHxG^o zS?qox$lbecS*KmTR-Hl?pV2kNO0x&(+3D=M7nk}&&JQCJYu|BRv3`h2vX|Pwe^li8 z)|$KTjk>@4pr-=QpNNH7d){06T)*wKcOS>|mos0r?!Fv&&LQS|#V!@Iv}<^d3k#ZH zmznEp>iRXkOtv@H;j?5c;;$tiii_YeJZE%P&V~hE0oRlJC(nx?q<6Erth8=f&Qfld?|3fknCmJoE=q=YjAi_5EtDw zsPTH-KAQ_cI%aB2+0rzmHF4jFU4tSzIqeQBn(dj6qkXUSwNr}M04+6#ahH|%0^6?1 zpcb|KW-KmRBo>o-S{Or2=4sITzx=B2;sXS(d6~zA6ki3I~$()&`<~6jOJSm zJHmOlz8avp5aAq{HzFd`d-qbEQ1qsgn5H_a`#3l4_K2Vc(;WSqMz-o)O}?w)(Xr}W zosf5L8XXREF7SI)_gLLEXY)SQ;eDzGwX0=>v-eGFJv+@MJ#~b2&|zP1b=pOfAf3c{ zi)~F4*GG7JcfNV>VvwzAC+Boku%2mR`G?hA-P1>a-7i}L3+tzP=7VNHuv0G?Li;4& z=3ZU4J~MJ^{#eYAaqa&W{8;{(XXB@y%UVJ_-&_*($}!L?3_L5z7A37%?~3lM^)}wl zw&nlkL2(Q0$5}ax{1d&@X(KSk>YGLdUUl=D?Q?VR451T!=R(Io|xIudA{(3;q`u=)quZ@FDB2c1EryL0-n#PmX z$yK_}R-xA18Kd&O)vF=kdR&;Zx7zgr+MHnQjtW!-Rl120-nT|H_BvX2a0IGt2*AJ2 z>potWv$Vi|Yp0XwY8hm!cJ;P9yzjAT z$wr>oni`{-Nf(mc3!jh39OjpvTHnv{WJz_|)4;a1Z5L*Lj!WJ7F|+U!&(>{o{Y!NT z0OIZH{&5SuItTPl?vG8Ee2C?h=8J0Td>#cD;~!?pu8->0IpYP@Wk$=5ZR^%0R*mI_ zwFPO9(Z~6+OUc+?fuEyQ<260d;ZmG$fj3u4?2oHl&@;^eG4n=XeD=n{Cds*ggbs** zCcFAVkOBVVPRtHYMj?six~3tE`LTO_Tup74I1#cUUGAA^^SLJ&=F-6&xRBB@_WD zcL#&tReOn6(ngeC0&=|qj2qOsn+wY1mV>~L9H2daz3TB;L+X~)Yljt_N@*{UVrXV5FyVD29!2p z*S&Ylth0Zxu`)AF?25MqH4p>eM4Vq8|CL+ju$PKcg-!R<2+Nqo_{^Jnw~t-7^xc~u zbTjW&;NCX8t+Hj$bs6_0;4y6a^~}@Yq^}Hr_KL~#{QKi&8wVxhVf^#2(gjn@;szzp zGu9Y2D8t5=FhV`k(vaqCfr+ZJ+jTyc%a#)O6;v17M0I6Gk=SWyu|4ApJ*dJa7_G+W z=-4`+TfHFa#ct~r;z*;8@+B`irY5=7kfnbPRvEmyL<`priuE&Eq!*{ zw``Mpto~d`H~%DRM{SIq?YT1|=kT~bw?^EV=~=z|SP+M+*_dWycxGd+zG=zA#fPg+%{K147za4! z1_!yP=iCUvMs<^-u0Mf@k83mNxPx-79v}Ity3WV&%#7yP`pPB^`B3;Sg)`QCeR9wp zm$&=@1?ZU?CxZzZ>$vIwW1W!RK?VL^*iBp*5S8CQ8S4Vw9utiTR%PG>p;v_`f#nil zts@ev$}Z{Mv<@O3ndTfGluS$l?VT-yBHt=boeMC?`y%&(acxsNG(5K2{-%HP%6J;O6WqywLR##I-Z*>*O4oa-t9fni9DKlkgLo0lTY>>cqV*IY)^^t}a_cqf` zX6oF-zCI59L0>)f`pucI#$jfiVmC&LD>>x~Q5;k_dEQOk#PzP?AKX>W=4v7=pBsvu zkYs5NstW9{o0#`%Ky&834i}V#sL|0f(MM>&^~P~liNw?ehcwp91XAz_#1I0E zqPS4&Aa}#V3TOz09MpJC^m_1dz~aOELg!tNNED{AZ$zU1Bw>uEiIpqjzZ#j58GYth zX8Xa+_J6q<=Msa7dgZ1nT zG`|kOSyA4Zjs1h%CtL@5bKy^fULst5&$)HA{XE{L!kaDI2dz^_UdTIaYv*p6JQ2^) zDIuY|Q_{WgtJK=q1DPGVv2k}5n>F8`3A-rTjC!*NBzMix*)^!@5kn&C=OD}yT~o8p zH}k8yg5B=v&_l%F?A=*lJIBY%6he+pLHIK(ya7ePAq1xa!_zK`2fi-56c+}|z#oJ* zE-lE_zj;s6Ik=7Tt6``VMg&YLvhdeC8RH)jsF+91R>AY1t3U-HK2Of*OG9(m@(HY(GIwb@GkJa zbq!GY+uI`O=MGb=BwCdx3Ke0Ct@3q z;F^>mBzYh&CB^3PwGuz>eC}zMWb$nlMGb`Z>kk0I;oFc{!^6JmHlzVU(I0~;*n+(= zbXbN*On*#<;_O|Q%ggY|+|9n0M4)QSIzqz5zQ+N^e0NZVn8@HH@adc4H?ByKeByjr zKHg%igne;-@dMicaSFoG8}dhTSX;F{914m7Hg0dYSox;N<`3EoJKl-?+%xsA1JYu|nWu@KS(%bb zeeMZQH_6LByf0^ffMFtbiEAb2c|QV7sxg0O%jDe6ugCH%2i$UpK3fXhW1ZX@sxrbK zuoW+G9yN}@uMgfnz~3xJTy+F!Ac*W6@z_U{U!1$t-RiLqJ|KB)V%`CSLp~uDE}Y8o z4kH#KJ`NjT)YY9g4$|wa_V-T<{bg9#`L8A@`ckSYaGDvO`F5gDsa@Q4@r>ExSO2iWo(;gh`8`FKlOr}Mb_dYrFi zk(l3HGJNkFFX%&k6sO%EkHE!1!egn->t^Wu>-MgTVlQv)?gt)#6BYfO+HCY zXX{a7$D<52M}N9VO<{S{@U}i3y<#^MpUP@-n0U&)P~+aG`CnPtcb|Ug&nWXKQl0o` z7fjx%G_`hX=n4I{@~3_Adwl~)4=69;5@jg4N@CZF?_j*6P zp?=e|f2{7ferSNl{Hr%Ecb@fS;&yMBZw^HG&o{7J(!*?Qf~iZQ#n1!`*TnG;ZbbBO z_Rw+ma8q6Jh#VUbtTXn(Q1$o(6BoX(d@$#O%^n?nTne)qAH=*UvT*fq`Y~zzgHu;C z(p^>khS$yE*DPIUM%vibWu)sZxe}KiXIT|jV3B0XHOHzgGPd>{8=&K2=%&*6-qN)R z&~e_fp}&`xS5)nKV;%jH4YhY~EO2xjZl_*w#Un5RzRac%2sn$4`+^xMbHZSHEe zyehBhLy!8dG0uj8ch~p#(wp0)?%3F|p(B2F+r~Pxb*B~;){dE*-E`uk?3q@UQTE*T zOr7wfjLz;sRk>^5GS@A8y{cSq_Y60_!u{zzhzU0i)?^d}-2JGJi-)DFDspT>T82tq zN=q6aaK%ICMOXq?3GWfwqdD-woW5fpFr18Z9$ZyAFRU^OUKNd3AKUDq%zK|v!hFmqf2JDsbfou?+$N2aq?s*v-g5^wi-JcI%ZRr9Q;(7K@k+_f`Wvt~qrKj@+0EqnFU|H1(`$ z>ayo@KTTh#WJbi18Z=dCer}F)Ty!|h)zJ72puE2Ec zk{-yyEOd5yQd*L~nVGZ7?Kb0CDSz|xE@QN&6U-KCN4QSGkc7+yVImw+}q5rdHBTo*@d;;&0GH6rI$I% zEg!8;^NLexau{oX=Fq)36-K2|S;l!-mOm{raaL6(^iOIjikfA)Zu(4(huf{5%|+ac z(~nq~OP>-3zx8L2e!~NLIIC)muVknqc}XWU*o_Z$PK=tR!Eoy4F!EbHvH4$y*1b+h zi%WOS>En`^#uaQH3>CL9v$0c)Q@bP{+wXB@=!1h9Jn!q0oroEExFsg04YgATn3;)1 z-|dPnH)geLT;8ItY1S>SH?vFFJ2YYMANqFa+o!x!mRE~af;1Ikw#_bB;jOWk^lr2a|vbzVc&X$(nKskY$D zDYb=Iy^ag^ce~6(0wr4_h4E2 z(1f(SnLQY`8QR-8uCUGw(DG~6^=ry%=~~mYQQyqiIym@|ad70$`kpR{iL;~2YMLY4 z)H4`$REhq?`t@DRqx=WR$Qih6b{`w{0;^LWZ86bz&e_smFJXa3Pi-Gyh4cEa&h68j zvw@-OIIDWt)XkZxQ#Lvw;D^S#K$|6+${Vi040oTFgc-E^cxd7}j)G$?UMev8&TO~n zUtG5!sVgt9`ffRfDTHgPCiV)B3=h_M@k@pWj%Z3yz%9fx68E}#$dhW^vxm!y-Y$K| zCIp~akKi6<8^#6uM3pkijmx7-1*@ATtSeoPs-wz=-&t|@ z6xr6bxOTbtY_0CdsFF_l`Xfcn8%*>|MyvaKxo+8D0=5s(OW0si^3hC7y46nqgEO9qr0B}ue`v#exJFiDggkGbiD@$v5f?aDxOOyOUH>_W(EtWEC2TL{a7(t zi@Ys+T@wQ=@!&*LGZ&8@W#x8u);-LY=dHNg6%h^irEhpd6a4 z66_OG!1VjO-YSc;Op5BIi6hvEvirNv#1b0HQa5{Od{u5UBVD5XX1d+FiYYvZ!xN0J z_;nAq3U-_6w#6a?&Ce9UV6u7-0>iPEWBXtB9IM`&k)Bb2tL!b02nG+(J;5wET}&Vs zq2TKZV+fcaPHdNus9%DMYY`h;2eI2v5WL1kmkz(vlrXLk=eM&C?$MP{=WNI^d5%P` z_o7egn@5!l!2(j7F#0hQ>$~%PC;j)Vbl$VFV&210=Zqefj~M{#i#AJYeJ>qNnzU82d@ z1bd0q*ajg`MP!mdKA!jkS$Qz$_ElAn@Y$9Uw#C7HRlz#pS2A)iftfvARKYM3v+AJ< z0e43yB;p-=QM>|U%x{$>DXS`s2~n|XzJpLh)HJLP)oYL(uDs-(0Gk1HNM!o?uckyNh7l`7X9 zk93x5i9`|t(lc_zN}w_xK0~d2s;Y1dSX|-Vp2pVErRCtFe6Pj9;NajM_|TPDy*k2~ z9^^$qK6CIu33OgjZ+!;gY}g>dPkiL=`VC#w8~D>-a04-lRao`VXKI(Ih%IR1a{7c? zxgi%rw=H>_iDZWGi~w9@13=*hGw9P?Wa457HPZODeVYccA>Qi>JuOJfuv|Cowyq1f zFqBa8Akbyz6;ry>Lukca{YG_>72bV;yw^f@`e zqp_XxM=J(=q;s2qYrvgMdN=Lg&Hg)2oIe675gKT0oExtN@yT*_a`U9{jRt z_?6f=xU$CA(9IB6+0frraRCvH^A`W!&W18p<)8lm%!a38ISO6quMMAS|K9pG;HVgc z+Md6HmcD)9OGA6Xj)XWYb4akS)8qe~Ef#yqGF0#%0EDMQ6HLfo4hm6y1#$A|w?DlR z?H|+U&V<~$^bJ3H`>L;pxPVY9kHB=j+|b^HbN&P{#-Ry(WoWoW6+CAsLrlk0)EU?mO!4OUm zfVImP9N#_xIIs^8U=Rwd@!b+IuXnhPb7FrPPukGrMWLiEu2}v|f**+;uaeI+!uKtR z?^h((<1eWjw8kZI{1Wbv9N@tmMY*I*@H(tq?7mze02V~ZBq~S_NodqQ;v4C_Nudt% zBa9^c%IgOmaZbklHWR>2LgnUBj<~3ajkrHJ#WUr_BzeUW70tnY3tGJTxbz7%j%>Et zQ>LYe_uZ$WkRwQC)c%68WNt(fB5sgkSOf zU^rRpFYBgx0Q`*)4!ZUoyW|RLOf(JW_MkmU_%|={%oUYz*UHn|S5H0YFp9M%!`-|H z|B<*`V9z}RC5(Uvw?3g7Rb;5

WaL$ceo=^3vk^b?>)%YguJ{@n8{cTG7Zmvn#o^ zzRqvLu2lY9@GA60;DgGZ&Tn?VDq1rO5+Zn;&I2h$I9Y={$Sv|p!J1i`j3;riJ~K5h z%lHJx%>_s$%Fl5#!kE*i*{YF9R~P8a-NhEQthPyLS?#Rqc%l2kZxRmtCeI?H_{Vme zaGe+R4+3B0@CkdadPIsgVn}r{>_sogi-A-cSbA# literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_rearView.png b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_rearView.png new file mode 100644 index 0000000000000000000000000000000000000000..e5723bb16684f529e5f7999a8904dd24ed4d5b53 GIT binary patch literal 100351 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_vehicleStatus.jpg b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_vehicleStatus.jpg new file mode 100644 index 0000000000000000000000000000000000000000..38be4bfe57f9849fb7eae6853390dd91a3451054 GIT binary patch literal 199877 zcmY(r2_V#K_&shJYr<$l_I*%clzq$Cg=8u%mM~J;)!4TPWy_WcDMC$)qJ_wwb&M-@ zrI00S8DuTn|M`sj{eQndSGKy$yzhC>bIy65SAva|$xco|P6h^soo1#uTLuQEcMJ@S zlI*PTm4VO#9tLKvL^GU$T_EGH`pYTjCazBY*($$;U+Vds(|MuX?2YNo0@uFb+hZ;l zM&{*CO&l?{;%i}cPTl9uBwJs7_fTpVuh=YGJ%bC1gd$n;u_o7y87u-*o_M}Gb z!Jq#qaKDf&Ox&j*{N91d7PnX_U8zMRk*8~__T7JU34I%W8cr%Qdp$>DiAxoJ z9v44ji2hElfv}c=xVk5Gm+KDtoWK*Kbev{)IIcFt#KwyHCdATT@|_|UO(HUidpz7D zA{_4)qp=ppdL9l83iAc7{LR!r)8fGGqEF<<{GUj^q8PEOO*C{qHN*__G<->ewGedE&zu1NQ{( z`NH-l>Qq#3q_|DhnywP7VBOie-SHN84({2zcavFqFa0GvKEsypt(2D;n}KD=8zRZ(SG(vw{E(^X~4QYPIB*XpX`91)>+OlB9!s-$fo zy-JpNr`SVNU$m8W@8`RJ;hEKt@LF10v&@_6ReBG}PxSqW<92(9Ed$Efal6c{s!`f2 zN@5jBK*PO8l#+OoN(xcN4q8%4x!41E=?O~;H$K=w-)Bg;-joM$Q z0PB2233Y8CY+vBcFa9gaLv@QEi!ACJ8hB1$a7JE+zdy=IyBD@>Kk)r~sYC(KqLJ(4 zZ!^nwuS>`}=qbiCy;n;zJ{~9vi1fk*NF0?c7^O|q@}>g5k}PF}iM_}NRweK`8U0c< z&ng7neJkDp7f{mk*ckUV<6{#0fucoo%s~7kF2)!K-w0C_AbFI5(H3JQA;HM>Fn*FN zuCe>|<=%HWIXU?Ay$ZCN0&*CE!i61G!W-c|=o?CZ>T0(5J>;8J{@GP7glqwC6DnzT zA9mEik4hQKJ&x}W83rc6>3p|<#xmEiZE z9ZF9fIB?(@Wzp{SDRX->%z*^cy|<-ddCuS8j}}A^*qReH^OhwFwDPPDOMDv8>X~|l z&9nM2ptZKPcAhwki^1V!B{XRV(cu7QPoAFW-INV59(}4s- zoD?IxL7~ma?Uug8zQMuenHf*za#unE>D_Z>EPeL@0B7$3O4BdoI8o z_vvcqN&`h*af0&AnE=aK83&Ak#~<^jI^aK}_!-rrgq`*WqAOU>u{ijw+=&v#4=~Hn zbXgh(MgdB2Uf+tnd;u?Wl4C0oTPx}D0sZHnkf#@e-zUdEA}tvgc9&JH_)|4|+BAr3 z8=JwUy&?BK?!vKUeahGp^*R3Z-rH{Isp-V3(sv;V@jqx4Avfby1(Q&dcKAK-9a7$k z8KL4#alB{`b)oj9pS@aytVooUSfv)0!?gM!W5QX!R0ft9wFl%1tpmi8G1`yZHjN(g zPeH69o`l6tziX!ObI1M2^%$eS;sI88JN$>ge>GFIf3xr3zyIm;=fCTid2H}|Op=A1 zVL8cUb;TmwlZ>MZ$lHd2yT$bb0FFoUE2FeuGczyWzHOn=4#Li=9y9Dw%@zm=2~j=P zxVfndUrIEVywNDG=GE^+L=HHAk}>kAT7Z!Hg^4YcizImMcuXk(bGTnO^Ly9;b!Synr{0?;JcYuc#=LMrZw1gI!{bOs3Sz zbblIy@O@_X)C%bh>g+m|lrC$n`=Y8!p{lB?BBTP=vVdGuQ*+NMSH0UBbAVWNe$QI==u5D@QwEpws~5_=Nf)LT0g#YY5CpWZr(dkMH}>AaljO@Ed~%gJj^0@ zqaXMFJWj~QypydUfPhg%;WXH$;0S5;|uvxKgv? zyEKj8oEP1?T^hdhq(Y09Oi~u&2Kw`(zM|&qb+x6-YJ~k&b?>@2w*F^9d#;XINH09M z-l+~A_~f;!cy{lH84WAsj0T>gJ*xfHLFoen=P8NvftEe0uq(!eXxJx!ddHz!3YE%T z*d5<}5qG{9{tAJJ5*FbcFAaBYEcT3s`x!#>fFIQkA4|3x2rD3mXiW6lEoG9)UfF%c zeZoq5e)8i9UUtYRkB&3R@)Vv3U81hD2QmkPsPQ3kg>=`Wf$3zAXWB8ZT zOTiB_OflU*mjboSF+y3kBb^zxaz$bi)p(5bnWEjc=G6j3cW>{MlQ5gUOkUR*58l%BUdA2SusU#Q$0mznSVCq``VW;Uxp^T3m4bIva+&x z(Su}LPH!VjtmrFvkH^H?a`sGR6nkV_%2a3_>-k4@=jeQP_r<2cL6Op$zw%J#xZ(_f zm%jA%IbXhf>t}B%!s&!&XAO5+3!RA0Q7g0iq$c`*8XKvSYw~6R6eQW%kNa_22D~Fwgzc5glTcTWmV{#di89M6t zQlqXAI86J~D2ciLDhQ@1QUm~&UFOo#RUl7tI}pM~cz(HiS8<$Zcs7|lxv~aN@8rpo z@ByMO{LWr52J`~tQ*8?}Z7)2}XmG|zb1M`n-J!4U#iESWa5K#OMw)Fw#!>TxN?T5N z*4AOuCqhSdicN3H-DtM+`;uJwPtH}=j(Y^`bdj-Up-?pT!=nkT#k z(%HzcU3ETXKKx7$P8ggn*d}oaSWn;|X9A%3)}fkKD4P}6W(3ckJ$uE|bC~w&Y7%Lf zmM!2xu5sa|B?5b^20u(s%PS}-{K&O;Z~*>D$l&uY+D$6~BDFP#l0+%W zsK~eF#7GZ5&*0O$h>@;2h2Zg(*I;$qgx*%r=E$MV=qIO2N8qWzQ~Qw{1ji|8BtbUt zzdxrP9cAgnydMKwLEnd>-Gi2x*a&Zo^yV{jOto;)Zos%qItLJqrOYlsT7~Z@Yj3IJ zM03*qE8gDzqs=>krGX|tT|IWbH(THU!pEQ&q4+6dx%YFiLV-wRG{g_IYeei{-G_f) zK+d6s3s*Q3s%Tz7#&^r+BoEM$=R#|U-68BGTrosTVb+_W%_^1e9NPVYNp8i7(0_~6 z-L6qyQ)7j!*&{M1dU#_tLeC^cWb8*)3Sb8hP|6?d_cK-Qwuqr{XLo7T^eJXPe z-rlyAD8iY;7Q)}oUwfDlC<#kTXAqjKM7C84)V+(U1BKGWkL=qp`+>Gaj#Iw{(;2z)~|t12X+a+f1vP-9Fip z(%b7h3D0bnA&gkdu>PiMi)-%U`CgN#J^KFC1OaR#Qcu05CMeC#te}2!bwaX|l6D?+ zsBTTzC94i63SfvsK~;ayGxg6@fDIlO(?|#4#s7am>_y)%z%{%IorKTP z25@`>G(QZ0KnD2*I78P5Xi6iiWm$4UB6=#o92Oa13HYQ@!4{~Hd`_Elpv7UK>2G1L zGWd#iOGE=1y!}@6eYWT4(&;m2E_irw;EQ^H`u=D|zZ*E1XBB5vay=gEEwVu;kv;O3 zDr!25H-CUPhwnw-adcD_mSoAF((-F|)_-CA*u99&pd)H(ef|AE{x1DIR~w!O2koN@ zP&{B&`Huhi8_YjktjuQ}9c!m(-%oxneKL)uZMqR_$7$a*+;cdHhv-UV-NsxIUe@`8 z*5_MND%K?HysOqq->rpoUa32e)X{K1>eJFv|IPKFlc!E0wSz}{j-kXVZzOLvR2JS} zxOz3yq0|HudlC~{m1+B*+ydqUwAVuY2uWb?&+`C~%EdC#VHhcO6vTs;FdPjiWA~;Tg!z{?@*>R}4ZlO!B3sf)c*~90T!PP#$d0hNGsE3a_U13?balMOJOarioYPxCWsU%^P^HfCC;PYZM{RGI>e@X1#)P;YK zr^$2M-+l zihuVN77>-C~@!#ok$Y$Ab? zfH`1+ehd0LiV+HwU|ycEL-3Sz&C)aP`A+Zfbif1nqrQQG4-_Bb8Jd*^-u~>_!rq?` z$cw=`!S6-hJMe7p;v;!@=>PDcaWV1wxWFe8(I?OMLNWFQk!+V4FDBtb1gywP#Ipmu z&<3Twh0+qQ;QtVtNQ{5+?wxt<_Y$&vvanV&NLU0gWO<;*pdc{y0G6jup8{!Z^Kr#l z^8__p^Whh^oM1u#yH84M+JRfaHCN9y?6~6PRaRNqmb70DtPY|B<_U@3D zS^zRQa^#3QktT%5?kF~qq1E}(r_Y}GE>0d#7SVyi58w~b+@?PO2J!(N9&pnBxycS_ z$*|3?3R;j+`+0X`BkYQHGv&p6Hc%keL1eFIQRn^fO&9bWL9J;=D&8|btkuC7A zDj2KLWVM~NWRoFUAPPJJSDcnQmgz`Mc3cv!OjykVc#Z7rHb|FXnU3%rIUE}oq>4r@cRg4F=y6cumNdl+gS2onb{sWS_-w$>m9XB_(Sg*IYph6&Y8BX67Q;agu-~ZMJaA>eAP@+n!e{&vszsUD} zbNOv$=pU6+r4DYO%!hsAN%O{t0bu@8O1`8v5h^*cevbMFGo(ORF1>o*u_|CHt$c3w zAiY*gPL6!I6-JCGsoE-~Z94vZr|Uwn&~X2^w-?mzD?h6C@)ws}y@-1zB0({1qM@m; zUmy0P251T+f&-*Z!#Tt_AcYT}ceb%oo)wS*&XQzxT`p>7|DoEl&dwaQ$1BTU_RqXj z`|AFxHf1J-A2{qZVlW6d4g*|*5CByO(2|*H+}t;=YaQ^Ph{U>N`|c}rqwufX5%)hm z{vuaH>bjrF$+4@P?NLw7Esoo8%W-m=TNr5Emy?sj7Cd5)_tDo!^BNfOG6I^=^j*T) z=mW0oLLPBKjTmYloA6QdvB8tcBiBXO#YF$Knt&!P0Wt6&G{%vbVE&nR*sYTM@Vkd1 z{&Ya86~-36e)p~&Tr8{xAfxuX2|j@xvIPo+1la;~n}on35|Dvxu-Z-}H}I$gA6zvW zS>>Xx?$qpjy`I6pW%_Ovl6l!>Hu_N?$&+uA)BKG=QXp`mKVB$QAazgxJ~SH03LOQu zphAmajDu{T1N1s%Dh{PBpwcg2zKopZ^;O@0|NR%9f;&)8QIYijRTm>(bdTe68WBZB zySZ<%7x!ScVdD_Cgnxd%s(GyA4g#4j-8DtPKh#PRqV@{|%fh{%cn&b4lO$_#@lK=8Ad&Kou`cDL| zRDJJ+_v-%RTs%69ykv9a)F9M?l=KQvI6lPwJP2FVTqud%HOhiXk5(=%gXO2kG1J{5 zh}Um@(X52TU-PWQ7M(ete&Ln6#`=0+A0HthAy{UT7F(`Bi5pGGr_inWd}5ZeOLZj| zsggo$P1V3*{p~Y*F>-7wob8N8bfB>99*d-Nkh8Id*nM_(Yy+dSv5m8FpnrW7L&=_h z)K$45hx#)Yr$<4=NOz$LrpV{*wz`*yKIIS;i)0+yOP@}JE`i7J2H?_2)-u}Mg_z!8yh@3mhuvo32ccbi`n*S~+@Aw+DE^zruL>CXdBx!6$8 zoKn{)4jV7*-YiT+ww}J_@FrB2C0DEeq5!tpvw43<{{{WxOl+L14Xu@2ZDpX5iub_K z_rB5)`G~VUQK?V}C_+Cb%P&VsQi#_khPgpF+UGQ{5IeUzW4)z*WUr);3I5Lq$8{s! z-?vkIraxh5MG%r9(gQToy`XUcx^!Lau`Vw!Z$z>!kjX&kATTJ2u!sW#paZA7<8KQP zj6H84#~+*QtHu&bIs2f0q*`GAzfR!tp&vkKh79=NL?UCQEnw1b-&mwpav??i?W5@H zIo-DSGl+uy*sf7X7Ahw9->T}FT>(mq0r8E;=jw*tb!kcV9j6FDL|6~ZaJ@dV&?M#K z%r&U#psyW3kem9Z0JMJ8EB>4LyG6`x21e(g&Gr&yvXHUq% z56I-wrL~PNrmca2+D%edC?O?w_6a>}Pl>#>@6e_-Vc8kGc8IV{Ba`1D(Z`?Hj%$Yp z*9KRR6!t}RwWR{$m3}2HKl-VIq|_^y9KOA8>-;Mti3Q8l>Do{sfb|l*JD&|(*fKEB z-jLS}yW8(e)P};*;L9Vy#M7c9#*|Vwz}T`M*<&`g7UZNW3H^7%35kuARVx9d!P7S= z&H@?C_L*mQXMD?e+W&w|c`WZwzFBZTGO|F7iAg^CW+c1VBX$W2J5wME-Zl_9qqD^N z#r5ZIL`M$k^CH?r+uaN_Uy7mop7fN4RR zCpx`k3BzkLbYeHWc=@tL?dpd^+y@;5boZt@Pe==g&XK2Ui@l1 zVbfhdDtQTpj#OCBu|twBizw^p_&P~;*mZVUZT4vPc=j^}Py{1G413fGH`H!0)2uoM zG=}<*S4C<0k5d`d&Jg-PTog$4aBGbOc0Y){gMI5}qn}C1jrJ3JqEaF!$7YwQVPs%H zI$l&%Aa-9OGBWZe1Czuf_J%}|XbkY|Q>wYijMlKflds*vyuFxoe zgEak)KqT(N79J^Vt`}x5liwn6%X#1Si}SnX-vKJ@Hu$tevdY`{?~h$p;=fORWw*!Q z@X1Y$u_I%;8Ch`bdB^U-5$Mkg4lxz6d-bWFJ25Y>6TwJ7Nkhr|i06;ynB4so}57_v+4jm4hOF$Krr zKKFb8t!?jNdP08X%9Vcjrd!^ny=z-`OMUVD;nOGb>ZIG_RamH<=o?4%F@LKKJo0sRt)GU6J9h*OajMl@K42P(kM-JxIzn#W1xD) zD5U}vlYbo}OQ$(Fb12e7mYuee(EWejgnJT-b$uwn=VE*59vD~${)B>@8D zSKuzrv?t0R02ssyu%PWwC_5A{uK^mTkJH!Z#i5*yQRoNY;czio|M2nhdhKG2Icbfu z6OV4*U+v2j&mKRd&%l^H{I8FnXOiRQnB(}%77O4x;nK}eUNpKu&+YuU#ycVha--jwrJ=D1lmWeO-n>!5Leu2)lT+5*tk<_(l=#;myno(cd5eqLOb1w z@GHrf{<^HRzh@PdudaJlhSn5t)4HTWi!nwEED;!hBrO^kfRJq&3?2ytj{2ZFEjjM` z_0b{34^V~gV)1%~m2p+&K^vL`R%&9m$|$c3HCwrrM6#szXBkvBUMkE4M2zp1R(Ok% zdyBE(_5=Wntb+E$=;)i#krGH*U}Be#X2;T9TVy-~yr+h_P4u z3KARg#NwS~UJM8dxD{gpAf47bzpw8UUHj7FLqt?B7{LGO z6=Vx=f1=$NDv0!tyFT1+ZCq&>l*injV`EceVQj!S!R;Rh`7f+DofC-e5WVJN@%RVi zR}|qB?l$;PH8(2_T;;8gwzPu&VgUHq5u?~%4lJj-{LMJS$RsX23y~uH!bM{6%WH0Z`;$I`cn(@!52EI2Siru1`GR;pC`~S-%A>{3{fY2T zAmCITWxd>tG!&osw&buv?e`@%2*bMsAMNNlF^M^!21NSRGcT|b*@(FUL?I;CNroo~ zt(d3Wyn6(RZ=O5C;@#4~ax0HhDkR+rDjL{ygUVY>RA&K}u_K_o51G)`* zVGwMn)y(hIT_APMeL7+N@;!WGW&Z_e=H`n+4N;rp$=~?p?&vn``5!!17xcCk3IR)$ z#$!F}y`7z%j>06J{tD4P_4f}x{)}QTvCbQb2%I$i7E}2(?lkeY8E(F5G^A)?ZK|a75^%AQvkrT|)EtxD1Fl$dGa? zN2l#7MO zp6*^{cvzqE$op_ZLIrJmcO+OQK#8fTp+awl-Zc(v(LxM75Qq~nVR)@IqBNjQ(TL>U zh~%UGb& zNDx9^!DMi;xi@PJB`c3MGcvnS1N}+4jGeA{FG!bMcklLT$^9l|W7A5O16SUj_*-~4 zl41%R_wr@8nR!4KJ8HcLazP|-GP1!3SF*7mK=Yy-t}`=51CM~#&_E9-;ZV=m*@c)S zm{RU57kUUHsC;ZvA%Mrvw?wSBAhFtZt___-=y@QM6u%E`rCEpnPOs1cTleqWmOIa8 zgksgKW|y8`X;&3h=X}li8jR!Hju}5<<WvJiXQIVa1bI=Ls{u2m%_*Zc(L`@5A- zm9q=FA=GE&Ykj_a*RByFB7?QBU4t{2;b!K>TI8G3LiptpF=C@8lD#6Ar z>aN*=vfav-LVWC&BkE?0G5P^9sH0+%$}RACaN+&+>O(a6wq=Tl74ok6BLe{zoQ8|47gPmd_5_K3AuR`yivSrKA~2f) zGMU4y8sd0|Z*LaAz8N$9_U2&**t&0Ty%)Y-E}d`Q?``S*y=J5R_wV2Cj-m4(w#FbN z9Lj;i z2;y^I;JMdYXe$)Z8_e3E1Jxb0Vh|3rY7r)CCTj_o_-Rt3we|>?oG@V7(}J%)(dO&c zcGSaAsV;H0Rai-hKTEnuz$wzdDUX@exJ#+fC7V&jG62Pt9u;Gjjp9p7jWL5+j`p&q zn@qm?e5Sm}Ko9(6E9;Te)X8{nK7&J%@lN7mz=3dx9{&_1wp=c`Ygv~Eb_f|^uwHMn zhD6EpZ1UxAS);I=Nos>bY7q-+e;W4HL{@=G2HYT#I!uE3sTK%jEy`=shN{&@&noRr z3^7l}?oA{r4USX4C9)K{-0;nfVzg?Rs=RJ3W0%eWs<%vWiX`wJ_ypQ2VPTNKx)bai zq6AI~ey2FIrNOkXG>PGXiM@_PKqlLv^iY45W3Qn6$U?$EE zj(vX8+xPWrfuhB|wYeS8+w58JslEoDO_NksUe)O8&L#vk&dpv$5MdIb#}L1feTd1~ z0#NplmNbYcVpuQH7doOMK|jzSuhP`hY)1e*LLT3;Bt5_Jp|>Z?PC;1gxcLNH&70j2 zd2W{0JIzHTw~Y3x&wENkO5;1CSLnF@!SP&W_{QI&D&p*BM=vzr^PvNzt0S25`=Tm! zLZr6!;;ROLYsK%?^w94M-z{Fcep<@CRC_`5Nj4r!fB<(Yqirx|>PmBfrkig5iLShO z*8DC=QV_+&f#fJ4oRG3r*T}*1zn>v8s{i_>GmP{=`nnb^AonJ75#&?eA?|F|(Knf; z*`EF|O@PxJm6j~

    Vh6zOYi*22Y1;VKywQmSKQ9!(W6Zs96)r=uu_#NaF=#n>Q~ zni>;XAP23hj1FlTW{&h!oHgJ3ch4U`x%pMNdl#=*_b%@p$tbiU4(bjvp@3w7RS-=$ z^xCCkD$2pf@T}MI%q3WdMuUjzd2^uUwP(leH=a}P;W(~;JhZ{*xIVHEHU&@z4R?^p zl4BQ9jcI(3a+9z7*R9GF31s@$otD$MpveEdY4r$2i3zrEFAN1kfwC! z{g~jz@xA(P)3oFgfA{B^Q)sN<>2a>d9>>upD0nn zO8Gwg?@aZ-Pl|_19Lik5u`cTQZIgTMFLZb-Iug9o5GG34;tC%-74A0n_4Q@z!S^96 zyI?|VTJKwbf4^LZU&Wi6=SeMxlyu9R1J@a>&sDUZu-2|8&1Y9?!DtOIWxx>^l{^)C zr{>qO5=-|J)4`%Etlm(byafSGV*T^b(27S=Iqhg4sG#tf7`)l;xlQ}9i!sTHo%SJ; zJ&f?je28$iGYA=Dyx2$mFg#)R870rnWnr#R&rmXIM+;Su)?v47Ma7`fj_+mZ=;%;9bjXDO3&+uYo1IvUkL^8N z&nV6tzp_{8rr0EoSO0&|vO$QhS6lQt3AJorK7%Xj()?D_%s@y^ZS z%*>EV^emWtpe`SIKOdL~J|{n@u(-7q54h>Rd6=Cz>|lKd)vi1@ZnlA59?*$X8# zj}}NmF+0t>3w6OlSk zQZ|jh2miQfpo$nYHueM(2A_vu>0m&{=T)>6{2#9BLMkFDFsMZzlHF}&fP=Q%cJ_R; zzzNzxkl6Zx217h4G8C|}@;!xg8zJn$X8^EQo)(8-{KSK)nsfc~kfTx49cr#dc}GGB zAILoQ`TQ?k>y)n0$tR8*(L6l zxLqPBUiiAfkO(j2Lc{8L?AlBcVs#uoAdRC#jK%_lgp}kuAIX>e=CBizd!QKS)Ev!B zi#(x`$0>jR$2U29rM>3CtguJd)9yt@bt^Yc3J?OguzM+7s8%HupNun~DN^o~ILsnb z>nNZDisU$@oC%9DsSF7|?wWZJh?&M4^6AV!W61;NWB~Br-v@m>Xx;%)UBwTlV`zvJ zMgmgMGOs*4d_BMUo%{9%rUm9dS{y?DyadY!fO`v|Dpj;-pN}sSy1)t~>oC@TLJ+DI zh$SuM!CL~~qn%j^#-)C2#JbZ;N_9#~+yZPuL`|w|2piXplNXRt${QqQk|h}O3(!<5 z1I6@I?$I&QZM0eBc=9A zN%3CLB>BvYJ%LAh9KYw(y^tFa`mmar7RV;tL!n;;tsN;nS7mjWcW- znDZc$2}Jj}Hd@TrykWltUZ}?Nfs{aP7&n12Q0O2c1+u;D2k;sqUZF^ege@M2!I{*i zs%`exfI-TIw`mUE-!u@>!I}gbysD6u6q^!6I8vbu(f5` z=YX-zyu7?Jp`_nfYrW1<>+^)bzp9O6Zn{2Wocq*7Tx_@=O@}T2yxWJ=J{RwIEx=$$ za#Z@h6_VwloYQt58x8E?J$5~WcQ_C=jLaXG45f0Pa|F~AyoD$YLZX|i!E7xGo6jMy%?Vqt#1Hxf=W zik)3w5t2q2i4Ty7k7Ngr!UO4p53%~%rBbMw!NjB9uy>sKmD#G%91Edo$6gT6R=q^sI6+)}@}pzzl6PVW3+b zc(C2d`mDnhVE0F9?kC{whiQ?5U6QfIYYXYx<@fiIPeV>KqoeuA>!Bz@G zYkIY{r8Ylvn9~H2Xi-)E8=0p9XEIMs?Tj-7)wS=C(+Vf^2{ zRa#Y+vb*~8uY1YFw$S&NEhwK6zL2d`ub)Ah8e=!C^aA7%!GP+J{uE#1ZkW8PcK z-l92{GFyKB{tIj%$6m{N54^j22@J=dDTQ-2ez9>~_ljOfi#UYDPt;D>-{|N7np?Zt z8E~ZVF5dD9a)SWMCxw75E(b?9oYWI|-Q>F3&S7NO<9}huJG`ywX@Tm)2;&8!M3N!0 z5Q*wdu3|>Ce9yZO%l_DqhAH*Tp2vY_VS6)*oFjXDOA1D<@EUmhalc1DbTXW5K4NLg z+yeX=LMjN&38_#nYNSr`vl0+h25L-+gQO$?%3expoAr#2+hjW%)%)3BAc%>hP(4$- ziS7nRg;JYfOfMJ>K?c$RK}HwQfiQw_z85NK@8QqXgM?;Sb-hJHy(Z*Zh-3ny??Le{ z!l39X#a*{?c9S*>V@vQbK}p<(X?zVKTkbfg01#hO>fi6pJ5azNjwUA03-c3h>|q@a z9D@-E@kTC7Rz6e3m{G>hw3wOBV=!3-MiC)7@P{w~ zK+0<=RN7;#p^6J+FZveQWI5*|(eDdL1J+ zLc9jvE$M3h+`nIYZG9m(Y!TgF9~mbSn%Z$kWMchqp_KIWMi}9W9~0%V*GiAH#2wddHi3w(*1&w52@ zgX)7rpS&oHY*{{fJAYY|FU6;y_R%C67G-7&456?|Ho<2O~*Hkyj zU&Ki09}UPIrEn=xCSU8j5R&_Z*qIqwWw5AL*M_GDI(;Mr%>RR-iXmb(r9j6)-BD_! z=oiQi$|Ojp1s*~NK&lpPH3SnobS)r)SYY~C_v)*LCpY1Yr);gJEZj~BjX6*Us&zt%DS-CDRQ#Esf$(Av2rN#FjkoHg(rfw&p6*Jke<}(Xjcb{lV)f zTHuUR>{C%`l@rrQGZ zKa&Iy$HdR<34(JDJ(3nnkAw^e& zjK=Juh|9ZbnP%?FY|NdTtBYz*U|nH+t#|SQPelkva*r=xAjBQwEmP`t>gvKJWEtR{ zsI)XIvyz9Zb&1aQ7u*s^&in+$itAMnIIsx;E>B-JX8v{7zQ!$fMzYdOOge_E?IFrM z%~C9VOiao~mYMHe%fpxqhO4|snMmRsj0i~gN?8`jqUdC^1JLQP-8qE#h5q=X;znPe z=-qR5=F{ih`6ny14pq-F>HSI9YYST$X!%bQ!b-!ez$(%g-<;#wN*!%4dnlA7XHf{( zaTJuHw-HR;(xOAtb&(Rt12uUsg|ofj{;AK0agn$#3aC_oClbY685nGOauY^zplJxA z@c3>Pq@7=NEeZ4%>o!#m{O-qY!T zZW!E*f*#k4+S)GQO2~&I8UwY*p>uHVT|ffxrV>9FT+M+D399!&#F4NCN=rE0?V6}v zXq8O4vcO}C(537qH!`Ae1i$uq!~PHR_tj>#&72%H@0!)=C#7Bxp=1Tz3mXms4XZke zb)VE(t;>#Bq+qoJ#<{vtOV!0+OG%x@mhO9iI0H1to*^0*lD|t4qQnvp!SQ2;s?dMj zmW}@dRd4{1>H|#Q4tE<{TS`1)T0Z)VdF^1a{?1P#-S15FhiK1CUWs(`MODc@*(j)V z^pm|lN{bv;2dkUx-})HxiAJjHe);es$_p_2yMAELay8uF=%_W01G;fegz= z!@8JAiDR3zj8HTxHRszEPcOgu5hJMOM#r59yN#lx9K!#&_2;?ghwX*F6R_*oyqP@D zr;17nyuvTVf)Ve5FeJ-MF~kX*ElT+!7!8P#N;XC8OmG^na5p-QH%g^{s}z$o{7Az5i}BWNM|%1HJ9S6s?rLbPuYxr)`?sMaI+twX6=B!>p{ohTm@@gGYx*F{I18#m!S#! zcv+#iM*z$+u;-N{F5W5+$Sc0YyUiYg?*GR(BQ}}d_M%6^$cCep*^c2aMyncBOTXGz zkR0DUAYLF{a5d#$$+`Hfxql_kq(nYC%X4P#-{E6JT&rZQ9lJ^=Gb3Fh{T~9GGEV4Z zMrLM!XoFGq_Kyr~Tv%=a!Vo{}Gp%eiVeI*VZ^@+2+h@}G%3PVuOmCRAA^3Ojc7$1eTmfF9HuzANe)Pk*x8X-a>@uK zCJ-Cqta}eb!#}CYe|W@b^H1&SzW)Q!sR6kE`F%C%`w;wQ0VX`&*Hu-pTfqdmn|o4_ zaO0F!S0W@y0^jfXHU;h*WnTS}%)nia@G zY-45$4oDbrJ*JEBOp$*gC9cVLRIdJ_TkO)3KO6zXv-kBCK`&)%k@H~;{b~<~t%R@B z)g{K~&{5aY+RB1^nlQNw*DFtz*0vYP9zt5LwoCxhR9u#+8A7TC7sPLi8I<7)hTys= z!d+b(g%hh^bG(1|M!f&yi7?SC7G6Y!IRwH>U#HA&b_` zf#l~)elO*x#xWRB{YMpAp~z4N0@7`6MbjI$s9`kYTJ|w1TKg3>C(h%$x^e#)2ksXM z#982eqMyaRMF@9~GuD z0?f^Wp=~EbY)oY4=3mL~es|VoyzOcKOXrs^X_BvHEVAVu?#-ak8?XT?_;P zFA@wz0d$d0%;x~k*F9@PI}Xi7mE?~Dy=px7JR1x?wJTzC0q(7=Gs1n@#r85+ZIKgM z(vaa?j3*t>>ahE~yG1!sl8SCo5`tHyWaYtnC7SY}3|RErfokc2Yyf6@BTPJ@Kc1B+ihk2K_b*Ooo$-QuaYh#T@$?;eTi(vjEp5tJn?y$ZG)c8lRdu~uAXi#Y?{hmuBbXnWcCxeYo3)~lu0fE9ReL$mKb6WVW?qEV6M&igIyTt?ihzG72Rb*tAwY4F!yQSX+P>SKQDjk?E`9kWjTG? zhYob+R2~YNF|x?{`liV9%-xF@CC-@PQOB?=sw+W$-Y}4zUjpr&;ys`IZx#%#)y%G~ zuD0kQjAXK{Fo5NusU1P_-uTxW*}z~o`!eXj25L7CU~WjqX2!5NbcyTB@QUe+MWNF4 z#W;3lb0H^D1@VUs6q~*6NBdG$Vhr zA3?(1kgqQ$Lz%XKzs!SNWe&4!w~ScVABu|mHQc2d%UU%cj#7X>?hmdY zztIBA&$aq@`TU#j>w*O1gDMjuIwtm6u5M)gzaB`nNK3O}Jv5mWrd2o?24jWWi(9fF1NR=CddLwrg8E`MsqXB=w)V zo^zq=d`8MnkCispGxb^qrttU=r)qxhfnXt)G!J9iu%?r6KUs$Hdd9~~-`@0w|1F95 ztB}9BSn_90(I3&;E$hn=iwz~n-U|#M5?5-WWA=AzeIjCWLbUeU%-2;YHiF45)pPax z0yhmsU9Vo<(_ztH>!-K%!%%djk0&%T7YiIn{tDST{TLmC%Rs+6iy(3a@ex%lV~LjgoMtn6%|rT+^qpw1cv9 zydnWY>|&8ao~#a7l9i88<9*i$s?1zmycvW@XJc8&WI4E)T^_t(Qdv7AU*l|X^6cKU zV_?ovgu>j81V@kJSYCNjM(SeYh=*U}5-lV6R8Ho)b5Jkq2QLxD zW2O{|hMyakJaqG)kknMm1!z>H!Je|-n&JapXULL}j>E^Cu8H-w{B@4n&7ZYYWWLOq z08LC}c1!QAmBodVy{C^y8}Mdx?J0P{)N#YDvCO&e1(($E(@}1QeaNx0zu1>j`jknvrwXOM;fKd;I|_|Q^vHx^2ZgRfm9V4Nrgf< zS|n$_KCUs#tW<%lSj*E?x%~uzv3~jeSy)@+zE(J{|UChwkj=Jr#NZ`=@Jbz00v0CaqVHW^Qc_e8e*dpVvUP6MPDWbxZEmG>~2&zIh>1 z>IgM&w05-)n<&bU1rPC41Nn6c?pX4r=BJ3S4R$zzU!#z3!VST3S{tjbs6LKi+JL|)v|U62kyu1=sr4xepbaMoN`g8 z;OYuE3TEx<4;~6a{b%n#%iUY@IM=dEHsfTpfI3?u(GOBWeKWHX*9WiCSX>@7O$xBV zxIlHfa{ynNQu$ZQXW(t6#4#j9jBRJshD_<=x1}H-Oha5mYD`&bMg~>_3PCEL2Ii_M zhpl-k9~)l+@zS@we1S6-4nZ6e4GEEAlNEc8FltM%H{2FXk?Yq9n)w9R@vUY@MYq&} z=0}mqjNUf~Oa=o-chWm1$d%$jh-S4#keg}6S36a6{oLRRHJ_Diw0Eac&911rNod<} zDR9xp7j(>o_8G{gHu3S*k<5r`{`qp&N_m!}S8i)Lquz?i~?T6g8@deUp#jaPLvL*baFo2lwj39l`zbv}d~ zIyVGCRLjKX(yULZ&4Ij(TazbkYci~PQW+TW)(IguNi&>TI=_w4ZCONMaH)8>CQ zhE%?nM8oy)p{Pt`z9w`U?);`~#_d;ozV#V?K}~)&6s>0XwNEf1roFK4f37+p0gjZS z!R62&vHOC4j~Kx{=|`ije*Ef_A%ESv{3=~5&wV^}r+?tPyO7D>@HQpx#MdgH6EyLc z^7u;w;3hA{Qq2r-j=SlyaD&=mS zS%6zG*H;o_%lZCA1LIpzLdQ71?DA6F)o6@6O##mO1` zgns8F5#FZlamHP)2zNjvjnA0D@4jfHkp>fZ5Tc+%1^^!wM;5uMz_9B)MSlr7^(+ZBPDW%{)< zd|qQ>^wiD949>ShY^@8-`H=6Zuj7%X<(pFd2O|Tc$QpB#DexLHyMMfo8njU+M#AR? zXbGu%_QCBBfRjk3E_cuhEz8!dZ(16f1a#s{3>-=fM1q4OJ0GPU7|1uckB*S9mn34) z+;q6cl>dz(;KaghBM(0tQ!D^y;(kH4(NrjTj8ME@-}Z{EUUW0F3USD{xo_hy53Q45 zpnfb9Q)=qPz54J<2Vw}oj*hStcr9J336S!4o1Li_H~mh>{+n^Y--Lt&CQd&-@SMCV zM_eF4u=!|$8GJL0xBgN@UtT+7=Q(HSxmM;4@=5!o4;cR-DmMtw53IU7!0X}(>thq3 ziX!)U6FwW3*^TXvA*2k6YQ_T~$5ESU=(D%D=QUdGwY6<%SM?`zpLwo)s|~fn^s8Ri z$JRMkFYP$C7Tpq!;BiiQY@Ks0k#IVf&?*D1Dp}9Epf}%K(^?O6c5QfcW-_VbKJC{1 zPVx{OZKoW|_KqY2L(9?*8v_7u6!lh9Dy89# z<^Y_eiCW14CDy_TQan#r+Ekifq;16Kag>c{NYa-Z17B9||9CCfaMI!eWt{5;wYPmS zO2o*mKsNDallh;*9byJ*qVXw58(43bCATJ+Lm+N(uPuxWo(z7{{zM@NKJgin)yB8e za%xINaNb7=#F1o@=UPeTbidC|KU{hfAgTdZo*&GlV`~4f_y2f*;C8=7+4sNPbE)u6 zk7V^-we0#VRH{DU8&o$E--=hDn$c2X`t-@!&$iFHAph!v0nZTIg_2IMW}Mo75Sa7b z?su3U5JAvCvS|llpwaY7Uz?Tyae^YicA;IP?^?P{XV)4#h$zt$f$9zA^ZVC4*Z~lL zL7oLJL`LtW02ukfhzQ0m2ndvUsr}dJ=)=FOKP=-1S|5p^1?BTuBIJ!ZspuS{ZE$R= z3M~rH;cN;JuIQLuT`#f>-@obO^p7YUL+)dyxFNQzqg`^|en_0Htqnv(6?*?V+by@w zjpGK2sA@fCacy_KDth>QRg2Z$rd2t>QQdNT7s-0n8^7B*JifCc3HY+wfTboTj_H3< zR(>7wNHHHW{-cc3m-XJ3f*S^rP`U{dlFgf$JINr&`5R#T-{CbIE~MOVM>01M<)1pq z0tsBP<5Tk57zK2azxEVAPlIOK=`of4%f373&flCn?%)6P->F$$8@Ag^2IYgD7Xmk5 zr&R%&0WvTm7q$GEH^XE3>b3OCLAH078a>W0eFiXLa>JpS@M5Y(m|if%FTlIiQ6KL6 zRa#mOL66|+S}EjD@1;D>4I>CRr#vU?%(WD8ocJkUkeIL5iLd`2S~2?6WAyeqUlXnL zv@X$>G$?v+|JLt-Yu>Ung_uYv`O=XD2fs9h{G~$~iGjW$YfGMz|IX?X3dnzSAq)x% z#rv#ny_&)aR<&0c*-KrPkPVG>3 z^yM~W0P6%x#=BmXyMO1lo9(t~V1alBm_JA`0EO|5vrRZG3N|p?+!uOd_`u_yPelW* zvCTr9l$e#l9F`~~>ZELG#ZhWuSlX&#Q}X?o((yp*VO%T0(i9#wU{k2nK#bTd8A1 z1GSd-ue0Z5{sjSOx@PR}?v0Q8B8$4fMeDSL!0D=<75VjbF#Wv{=A8i*iH!9Ngg1j5 z^;As({qMF;f!`Nn>2Sq-1{kC~`Ij4zwDAC;e1^G*$I*Q6_@UxD*&6P-ymDTQ{!-cf zQLGrGqQ^(+xS8OUjofP6wX{CIPMk+t`+e zPqYKaX7Q;lk!u+j!*B$m>Qi#*;qCs~Tc7t1Zc~n_9Nf9HS$I|RYE*`%IV-C#U7|vy-NJ z37zmibIkeDfpcoRXZ=p6gir02S!YIyF2;IJfKK9tgokR}3qCK4fo#VklMLDdg6|KA zP_|w&we>qz%!RG>r4|k}kV0!HjSElIrqj_f9AS7b{P1H?YQ_DZGb_WRPq%s=H~I01 z;!QV5mwJajtfNi;k{`(9f3s z%+e#Smq}cHQfsQ>?a^k3v_CHE(S0erB_dzK2&dU>f6k1)o}bpuE9FrBwKzIDG+b43 zDe?v>m|9f|BroOu+>mPuk=r(zS&=3}&00Wi_JVvqgQS(s<9m~Liu45$8q;id=4|he zp2|OcocC+U>ZH!cb~YSRZDqIHt~fjiELPM$_^C-e)1`g0c{|JS0d2`^!z4^V2ku#i zAkF*Dw8+lGSSkaqGZ?)m(@J5rl1=qz%UOp5sgIkbnogZ*d0IYh`siR%Y@qGD|Lrr! z-Z+Sq#x{CNnjPajhAz<;eB3>&s^wIlqxDqlR;cMbSG{UI172k8{pRV)ojy)PG4$(7 z#`?c1?DgFD_YA|HKh*C`A~im22L8)HxBH9r1vh%qIlp~|YWfWC8AB zP5vO$S1bZgi{R7}Z`K4`L~&edTU)lhcK?)$WNERcLtWEAy(D~BKnP*v9Ghvsvx(XA z*g|9y5bDU(pMJNEiJps|e@z}~@;4uYve*`9#;)F?j6sS{oC*7|YQ1D!Q6%uD*wxS` zhguOCjj7Yug(K7*@vm>at4HW}aPVR2`x^)V=BrI|ve!<RM(f#GYiBcvpuODp1wHLH|Sr{wB%1ym$2 zY#x$ZZP>2uL9Q%Tq6M@aNFVa-j*zte8pt4gT))&l9T!lYcSq>NhuC{Bdu6oq@SFne zkgFmhrsO$KL;2j^T2fvG%c;chY?W&5nNfy^au%>FeVYjnyANISm9t_*qh}IrQmp^ z)BN4CBBO~Z=lOtApzc9!I0*yIrS(@FV*AHK*-u;M2!axffpEBjQeBRgE*u)(&@-y_ z=yzH;HU%m*aa`7Ok@&kpIc?Tl@~Yx$BmIPIa_;W9v6I_JyV9hT+&1z5a`_#WM?ZHi zXcSBfN7}yg5gtRrkG{cF>iZKu$Y$Fc(R)(+91Re*{lEAFtYkah=lAsuL>3ALDXt6e zW?jd&zJ5T2YFKHip?`OLlIo(^VP*F+!AK`o_fh!Mgc;$9nCvrn?x*br{gI=lk{vVQ z0r$UhCQKgeEOi@6t`BUhV?!fU5?hYsp|NZIL&UZU~_vez;OQUSGZ|UM=ZSnl&33@ij zUfF)6u~sl?BeA4&uoOZK{{*=g4nh3gz}MwGXK63R^Pd%REWQQNnbT5=L?!=$VGy5{ zEaitF!@-|&)pfFunZwFreBR02;m1z9Bhv7hDj$O=vavBsom6m`*jRJ;IR`9WWz%@3}{2MYQ1O@!ladY zIM2#xYv(lktIS~S5~5Orun=1{PWo&+l$Mnl#5WR%wl_Y<$2B7wt^J4^i%;qzbr6C9 zY<(@+v@_<87&?6Jp_0Q_JT*szB9HU3y zl!I0%&F8$_TC!a8X31=eMzD#oG(CL-5_XHpt2>mm|C%J)`BU>Hw}ha2)D>{fkANal z(177myGd3Q^^~~Cv@X9lv6}4sr$zBY_0P@iVNJojzFZqyncmzE{t`>ef}VsB!nK}+ z8`li-F6z!DNc8^{H^0^^^Q9x7WI)suC3beCoSPHBMl$b|`C|~@hLYH7LpeX~qZ1{@ zX`Qz2%t*7&iT%UswKGmtdc4^Fx}CeyRpz=UzFbn@#(UT3u(c?Z6usnx@i{6DCQ2nH znh|Dg(f11vJEne_mW5^|V24wr&j_6rN;uPj*XDns6Ub(G?OITFNf4)!h%sKIDVK%k z#Be#wQx=>F5RtA(1{O7?oGW`~cC{(s8DBA$AxyDA!Z8J3Q&uAp)gxGPkAv14p7|mB z%7xUbd~&HpO+=a9VuncdA zL-eK*D%HN0uG?>%&l0UqZQVz)F@3?~hN)#%E1O3BSGz1@OAN#-2KW?m;PFrowF60s zS{?p_f5flf642O#LP4P? z9P6~tG$mjlCmLS-ouMM-vc>^jipk|nKwtr%fCdcb@M}l{(c_1`*MFf3EZWkIo?*K; zWbzm6|Nd(2M_70SIV?wyC+d7NMSdPVeoF(=BJ%w3 zQzzm!q{c^(5u2M5_QMv>&ytEc(Eg^NGg9-b`xe6mJJ0y0rwvVMUh{&f?5QmK|M>vR z@p%P)=oil9EP*)F@|VKf)6I|$LH-kA3cCIx78aMeB(LB0JSB8s5CLprBW(A{nhL%E zd(?|6u=3FH;CSfN(SCsdinAMVj9u<}S29};a*H}PFtv5toKA+Ugh<1INDUVHJgnGk zTt@tZRlA*n8s{B^SlE>(xAUWUy=WZCkkxBtduwBt_uvNGX@VV^9jijnlR!2A++p67 z3?%Pk;Rlid@D5MT_y&^$Xa|0Zo06WJ6n?mTapCM zi5c>pWBigtf?oi1H~2dPfQ-1vjwAtd9kF!4_B^*mw!?FYyW(tu$qhEF!BHAt2(22T0#O4lN_R?13ADe) z-Z{M;8yjP@&RJM2c?duiGT}=Baj4&mNCmPDlTSp)8=>DFzvCGTYeJuWeRe8p+85Gg zAA~o(9T=%(zs(`l@u>Fk(fILO{E}2Q9r%)S8e;cZBMk1{?mu_o!i5j{n-4jIJ55+6 zTj%RqtoL*vv?<8yeer$qz$!Vx@4 zNSVb=$ITYm#|B7%U}-7E4Pwrm41#VzyQXRiI{t75njPSChqjKn&PH+yI|JLH`taRIO4`0MZB%@C+`1`yN4yrLf=2>BplmrdO>s z58nl%;BPg4;^JcTnK2>I{Do~r*`1orNj93y+?&~JMWi|z zcRn&Yz`JO)*>=-)qUpHQPRqT8_&}sp^HSgdQaS*AfU$-i_1j?J?u!6~xZkd_i#S%^ z{HyBrUjm{HhA0mrLbnE6>^@U=ONek7d=KU`B5Ij70=3=LK8^nu8GoX8Kgqc-BDzz1 zE}(k7M}#kORV5`(-{7NL_38%UndA(wq|-u|>p*yPU}V6URRyBuyyud1#tvf!avSxv z=?k=*0Fd{Fg9gxyx+~oPZ>(SPs(H5P5$x~`2_48<`T|o@I&-R>IHJXLmZ4xZ89w$C z;rB1(SX{f&&d**z?nsizyT!eD&n-0Z7SSBR_TJ)d`d;j{w(RRrR_L@L1FR8=fExye zokP#b4%g=d4qIm{Fh=474kq>~npsdafL>tnVTlWMhX6%p*e@2EC7Y!V zCdt=hSg!nLh`*+~5Zj{hC?e~de)KhLyWkoxuWb++Fybe79+C{=a713Q)((E`LSM&w zg_@e0ftr#%tvj!N&|>`w0;m3d4B0>w{s2RUj;w;*S8m6lvQLWy*|T6kzl=PWuV%Ra z2I2F4)y#v;V(=M? z#{`B$G1T~|@d=HgO4;}IwQ{#9nm>b9fH44u@qeL!=#T^O0Xpazu9I}I>1!;R3L6D# z*V5hfBbx;+^IH`#3J3xXaq#Kj{ucl|0TAJA`rEEI^ObB3ihgKuE^<3Wfc_NI`` z0TwrJ&yPf6hFvuN9}s9M*`mxRQSv3fpf5%D`xEVZy$5@hbFSFx>RD%hcDIJGp_2K# zD?Q#~T5MdfDE$_~1__)oQ)XBI@CzV#SX=Ohj&s%#l$i2C8lZ@deK@{iA*$gVha>Co zLE2IWeG|wafr_<)V$oM!d5iX;bEL_&F+0Q)6QdNHC2==Ww}E8|73tA?_gw7305C)1RLrr=VK`b}6|h4H>04YMO&^s#9sFeRr#B<{%^I@y zSRCyY(b=ilM<7-j0@j{;ZxYtuMfc`2cf_#%r#53xO~5^h#cl-L-1WTxv(n2B{=m^i zUmrCUF*Rcnuxx6c(UNgMg-RIEeDbwq%Ccc;FPAzefzu;s3|eDKE%aRf(J zDX`lgAdkU(M2Y7paF~bK_Al5=8#{!=W_3K0LqbWK1ARoHCMU2!}QFGwhG{)xzjz}F{O)A5IU)?VxIUwZH-koXV(``IYWv*}b1-Ki1dY zE&n~d+rW6~I_ZFdsxR{LWh+i7om9NPhr9#t2SBT*feCSO^IQC_8i(|`vGOjD^OeVk z^Xa$)^ZCLOn`Lp!3=Yu*qUqJpty~$A>BiBWitVmN@2Kt?WKLD1TvPVp{#Z%rc*eYZIo2+RKquU&teAC(av5I<&O3khB0nl%|KABc*4}`GTyI zhcBS}Homkl(^uSoADDwf1Mz zmkz)A^6X28UWue=i6zWYJ5zcg&)i^7R4!PB2tuZ*Ig)2rd!Zsz;4Wn2@(VPiW()H}u@MI`DKKVI;%v^2qe*`A3_7E? zduyZFJKjDxHETT>5{t=x7}W@Va>~?hz3X93zg-`JE{n=*Y^C+OcU)eE`y=xI#;bj7 zg*Ce}Ae&m#2r_JL%T32OL0+pZ;mFPEzyIwnCCZsQ*n~r@Vc*YmDfT-b=5Eh7@THH_ zU%{!M=?;AgJOFb>wgL%xE<=T5Nd$pT#Zoj0B}jSL1Q&z)-vAQ;XB>|5Fk1pWftXKD z`k3VqeU9z$uzqfWzb&c*FHbuqT%>m^rYQv{FRuu4!5J_m?|*G%!KwmL19UD>&jFDv ze_-_|je+7>qvY*=L|{3G1@OUyV8T(}6k9;l;o8>&JiA&ENkIsVj+9<-f)y}t`SjjW zDVSb`StwL*T<}5$ctNNl)qEe@S>E!E-Ehxcm&=Gd)98%<1-qKo!ID{|z6)K%hPiBw zGhS?;p4|=Y-dS6;|25zmnf)%oYWr(vbjom9QJ>0$s?y1Vgg-??K}-N^m|qS;@#GgP>Y zXfkBW{mn+EM|X&kgt_I=4Lja*zI!{@5%s2xON(yTr#w`_!-&!I&W5bd>>|h3T3OA` zsq@M0d#ix;$stB}Jy2nfymyBAbzM;SaksU1R_~S?k31Mn>o9-La~ri-v*B&zK5@15 zlzS)vDlyT%*lB>!BLqWu+pDbx&jaiSakINi35A^Se&|GLK;S%!0?kW~>XHGe-<^@! zh8NU37WfKYn;W*mdL12b0Uh;6e$z^)QWZeF!nNTOl`L&Q-x&u06 zJ5@W`WWD~K3^D}3<-ebze!TOWCvfQYbDE9*7(ohV5*a{N|=EuNJH>axI?z;iK7R1Vhz zbPmGRXgu2}>GIVZaP5Bq0{LZWzNGti^xm?ZB>-n1PWd3Q1l{<6$I0o{))2CbOZ1W9 z4_6eZxchLR39&y3f@C8F=|vCM$X(zYNC6SEpMYF1gek(*XdxDbhC^O(H)v(H%js{& ziG}$EM~6AqLZe-?u1ufvrdKB~t8E*^l`m$W+BuVa#|@YPLco2>MpT)*ciVkhr{#9d z5MUo-JJf@41QW;R6Kv5sSy1-X`oewh60bkRn=5}i*?$fF=`Y_N`fuICi(+!`CZnMK z#|A1$2<7$@R0ur;$u`JL5a!R(Uh`%gm)sNJ9U}IKrwit6|IukH|6M~-xN->xIfk}+ zb_W>RzbB!O*=vOzIqx-yl~a|lbkN+@6@{?B1&>`^pA5g~_CxI&G|&B=FQ(#T^sxTN zFRxzg-l;8t+SIxLm^e|?4Z<+4rEm9}E`pW@Y5NzYrKHBQs`#wj zi{gPHvj{+BFecTFJ%$ z6BNd{wIM8vB1b{0!6kU()j+HFmoUAm+@Vq&OBy}?fp zE_)n`z)gi!&@=;KA;=aO44Roa4lNdM4#V(Yp2e}jiVh)6s;c{uE039EphE?7Q({7} z{`Pst<&}!g`)8PQ!3O*%@DxzC##q+b3um1^VM!IHI4O?;v$*@W#!So}Z{~qln!OY* zN2y2rSpryDWNy97oI=QhfHC`Y!_*t!gSc4qh=Zo;^3qy^=i=CI_;)29nWF`Hh+V771$=N&-LrSXwME(pA!)i~l{T|;1E0X-n|Fe3a^MN3)D z=D9W-<>h1RQ3*7#^LCzljV@4X^HRy@UoGhgn7I#>A$PV5A>1#!ZT_BS{I#&a0Ab3XQv_QZA2MCj)T8CD85w3j zyha*r?id)k*esR_1JAAR+2yh@t+KfN#>Vj2ueefM{_uKeW{UNNQ@cssmK`%r$HJ3J zZ);WHZ^~NtYgY)3WOKfrPE|Q2wbnM&+-#&Ic&R>_99)Q+IQjJuw*FE*k4!Y$g|Xg) zas$8N_uRGo$QKV-A3m~ha^d8cOD}I0e#v$};8M7tJ(}Ie_JybwrbCpE)FEooJ!e5% z2N9^zdZM8TzP-Yvw7excV+xXsxx2KLYCg_A`=aUFSjueopf)_VIF1Y1yyAWw3t~Px zq8$+lI^>BZJ^VdVoJo?ZrM@Vyx^awPYh0`#qWn`{XeOz_Z^FtCaxDh+Yz+vWrG_rP zPUWXhC$Df2nVQV-# zl$v&?nh7 z=7~mZ6|*CmErU-OA9l0tOud2;i2Du{+n7-F5#zqQ2Lkm)!l+HJDz$XT*K0sHX$n?I z#9u+FLYvfzfL>gRrkPAY$^;(f_XB<|49r|;J^w`G%s`5GZn2I)14TSFPCGdftpc`M z+^uLThjH*2_?8BY{$~k5YTgf^1Pmzpr*9IR9rcsvg1-+Il(m%5OjK&VNcV*3(I|pkTZa7I18Us-xC5B=xW*^S2VHK)V`fkO zW#Z>W9On9dOx>zlJA2CG8#V;DY!p}CZ*SWo)_t)76eP zU-Z{85j|JA@f*rfvCPx69gwMz`5>^_Jtq#`K`dJpVb<+v3M$DfO>Ic06b%S!b?0@z zM%a5=zcGuryL~DBwJ(~q3iGFi{i3kjsN$imFi8V6@xG~tAX+$i($@nv*iUZY%RQt{ zGI*i$>H9?a|h$e*pLZ`Oc_{cQ`Djg2d zz!GeLaT7QFMSDFg7e}G*3l7@4*yWYwX8*8x__p+1FCHHTOrI4f zu^fTE>CRkNh4oitt{b+i=|mK35z$pd!VeKlCl*#AG~g1^DUd;qkW``X4;B|?Y7q=3 z62FQc#t8g`4Nn~QmO=}?8eH#xC>CW}_1-e#0(-@eDbZ&a@P#RgDssd`WS~^h+{MW1 zGMf8{%Yg?o@*l&Ec_}C<(I`IdKn+M8)@=DAeZh*rd-Wr6gHnmV@tsV$z% z6|B?$FT=nJ3@~XoT2wCABMTiOncZ$sDXH=!cMNjyPq{B~!>+QF$4tH+60KXfPx0mI zp+_knq1A%*VQ+MQfjUrrgf}cxNx)Z+gI_w;P$j+U7*Ohd2T-(C!>xIwf4rnXS>TVPI? zT6_}H7d!?9d0VCM`JmyF;r)8?!EKfP|K7iLl91%&9IU)NH1z)REyGlISLa2JZ@x#E zeLp-Zq*%D(8c4msd?nC>shTQqk*AW@Zz?LJQ1{}ddE^U-%8tA+;tcsI zfiJki`yAEl%eHDA^hoiro~Rm5FV4|lG<}zTDK691a?Q2{qBesK+fOPGA<0JWVY3+?QLvH>I!b495JQ&H(dlnL zdeSa>rSPz;#GK%Rg||oKI$=RlF*XplcJCBbF$M&J-oL%dKjSLa{LOqFu2=6>=c+}sOj zbU1~$jOgU*V9%|0o}|J2amk6bZAq1A4vWlz(~Z}$;5amKB>8e(jqCUG<-b3-14$L~ z?GMe|8xQl$K)Z^?9os{}$+V3+@zhr;tD`zA*(v9vx_RSPOPH*v^p(~im|^|DZy$i8 zeP9sR23{?Q#5xpakVp*hKjKI`%EP656rLfB<^NL1zO@FqxOh2+kcJ1va3NlNy;xUEv4ZV&d%%h|^_n!3F*Ks5wdk$w0I8-r5y zAq#Z_0FjR+Ts66T9*GanE>97w;=2Yy$w+iKPcZ*#zWY{eNamrJmzkMhNvV?y)jF5| zchdt)`rP%D(MRiL1D3c(8ZH51R`20j?y+&O+8v;Lp!~!T`I7k0n829tgU4Op7 zfHHc5(e)=}YXuMj5_O0a+RtOv+QMYx9P$1`!u;J7sWGjUJSC!S=6V<{aI5H>2~CI~ z2z*Iz#9xaD=6Q*nVV7VUe%{~)jc)63SzL)~!$g7kixP7@M9O1Q94}O6^d(B7@w^R! zep?@louuNf+jIm0FI%M`*P3>GqnpO?!Tajv&LDh3q? zvJAPvc;w-F(|lh+ye(;(a5e;sfLESE^8nF1l}@;q4~ZSnPs!e<=@QK z1&BBRf;Z&eL)2nO<*x8ikGN`1FyBLVZV>Uwaz$otZu7}1+_E=PQhn!dtU$xILoMzd z&N~84RV;do-O+PSil4X5rnz~BrlP{GaVAL9A-XHwKJRpx8_8+v5Bn`2+YrEsVerzn z;7YFxvO@@YZxzoUBOL>Su7kP@v2JDblqW>%DxA$Z?2Rc{UA{GPk}7ntdGss~n`L_m z?*L99>%kkn0YDm(BClWi!vf!llgs$@g_na*aaM@3uNqc4uXzBT18Rkjq8`2z$b-LyA4DnStFyCKy7uU zaZ2ukFz-)CwSq*80>PmJzn)rrDoqfzGCI~VD+VUBmSQtGMYn|ZY z`crY|gZajLbhzftx=0Tv7iIxW&Y?Ko8q z)gm&v3El`}SLf$PF*tUeVW@K|Me1KoEx0pRu>J3k92g5Rw&KG*=86L{h= zuZ}1Q0;E^`O|x!zVO~N)oJk+JfrI$vcbJ!lCCXz^kCo1&aSWD>r-0#XQi~91&x&T^t*kIiFd9$Vs1Zu&|@F??$0IU&u zL%W=5=QRgD&)^#d+jU@h)(-U*L<)$ct^VS->dLrEB}Uz7Z~MIgw5pT>4}Ug}=|};$ zbhvog?&sU(M=Gn>J{IGnMqx0tI5e0np!`tG@>!u=^`K!4+U-?5AU``I3zIo#gJt`X zbb@S>bL$(SAJ!oiQ@2BofI=#WCo&@L3?u50YH*a;`gGc84^^fu_T9DF~owk-nYDg%0UH)R~x9amCl?kI1l8GCtA&Z_u>{TYWdzPwB)`Y0TrA8Ew%#-EW^JO{VF4sjY>Tp(It>0<0}Um3I{2s02+ z3T`?VE8e_>-FOIy`S%Mxdv&lHav}QS@0K{mto@wgw-57S*YuqV z*cIvqpJTYxex!o|^9L|T21>u<@7{axRtsDbGkfr$G4JxGIQ*Z`FYY}jzsXOWev@OH z3^5blK;bBa)+G&Zs6#aeV5(O=X zeQlUMmTHMhh6^I+v_03wdO!PmI+w)}KMD@C9FzRULD%uX3ficf(8>EjM?!XYW=u2E zE!2Ka`Q?xVux-weX^x)P6^&8zf0o8n75Ln_B;mKyiKv-0=>#XsvAZZbOqdrP=(njQ z!N$hu4_=IgWOlNXrIZH!1yQN;n?pK9m3|}>-yHGl{LcG*yAB=KHd>F4Q1v#vn)!mkTV`fa?k1QDDSw zJuIq}#fI1%JTb$W0(KPX6K8!s1Mm#2th62OGO z^a}SW9Hud$zWcrg^&s`lvCiOPHCPHCjm70WTSMp(7dS&k0x~F~!Pn^NX_&Wghc(01 zuu5|~LLgIk;X&Za*6e|c0ui4z0s9f)N{1k)EQjU)gvK}w#*B$8tW~-9 z{AC*-DP14Qq2T65O>$x;jV3`%65cC6UnW|dI~5hv77o1-6KD`XdQJ`pa+k;xga1u#DT7Y%<>Yc^92VdnPtCLjx&cZ{kXH$PP^fBgh0 z9!fiDt1er+mBu62k@fq}L%#3z!O_0hl;e`jG#%LSmKro!>gQ0!Q06`Ry-% zonz3U=Kw_>;cBRx7|jU6MJju-^0i%l&01}1yd10TNzPZreWL=mQGrLKS}0GWYZV_9 zD~zMp(Tw0cgbqPNd@ypLYdPz0LYXy6y~sR1K}+?)@A-oED-UNX7r?oU_{lV6|03eUaiMRefy?XiwmFC9QU zhL!BkV9ReRecS>fZVRGM1T`$OFfDEMF9>=GFUVk@dGVxJ`ZW%FDXsxOre2_>1CM`3ZNQFDk|<}Awb9nPKS9>aj&VN(Rx_0G>OyV5mb82Jn<|6 zwCv2@ikQB$A$3|osDThHR`?K>icYYchl_$P%u9jmF5r6@ts4PXKgI_#tskV~hQ-Rd z<@nQs!@~`uVDGvg1c>&>osIhA8!!mu0nPRg(Wd|a3&w#o&1`pizd--%_go$HP^fXl z)$-6Ys&OFo)H-JIIwk}aC?6U+9)n|pQmp8y0cjKCBV70E`W)p7ze|CdH`b|Y3H9Th(%ybMUQyr z%+SzKVX5ub!KE5X^~=Wl^%HH^;?CE_g3j4#CH`qSWN}ok!g0E-8!QVDAybfEgKhbY z-Htq%_F&nw4_AWWzz1Ov7n*uP+x;6+fhF}=*}Fh;ChI|uQE?Z*Do6{Pp4SjsQ^{ro z@~cBb7U2~x0d?aAowTJ!(%wIN$pFhQZd3SCbDqI*)KeyZ^A-3OCk>J~R4R_q=-MmS zlw2V~n5z9Ns42ZEsCew3{`Kp-R7-u@{^@&LwlNBzT~QFb5>+8DDEkusM_lO2?^h#K*)?Tv0LxlLHvc;TD*-8XqxTIZuUNc!XZN zTGUd@{pLr^Y25s$Sxyuom{T1;qG?#UK*lI z&$a(xAQJ&q5?C4l%mLJ4xPpPQ>fynJ(E9Y*8Kk{tG@Q(*RH-SM1!u}YREHJ?V!cqq z`9kwwX-F{{MS)^Q)ja{rV=NUFHqHJtQZjL2eYvpTucEf7{Ea`N3l0Mg-~_z66An`s^+{wDzKN5ZA>_(Yt&OGgBJii7lO z@Ec(<_fwaBGJJL}>;?Z?nFSLYy~ZtJrsnAQ?{p5y1IIx;Q=hr&_1tr)&h#qhLVXDW zC7Wr>sdAHlQtc2mn_`cEboPq*C>MSKoTV;9f`Z8%3aTiIAvOz0f^E;vWOg!C9=4%6 zyyxfGm7w!I5$fQTlgcLYmde<2o|S48irm^Dr2;rxJ)Hr@?!O4#d%tH+-)=3gRMf_1q6 zfPj8$yAK45zIR?2+s>(~su}}F@S*dtWBY5htLs{d2Hz^I9k^}3IPJE5BpRH)`i`wX zLgYr5T7nK;+-be;xa~*#oc!6D6TxL2TVdMq4~&`(63rAl|1TPJjtM5K<2ohFN?&uz zIZQ69q^8=ny`6vGMG3``A2bsF49r)g#?dOwzSKe!QupJ`99KdQMty@t8hb8^J9Hm7 z(Jr36_AXqt*)m2|I#n{R6xssXRmv~EJfZ2$Qg}hmz9|sHb>*5`jFN&h?-fj#3F%r! zZi2e>EahUN7!m-*cH(W{E;jo9%x9$wFz+S!DdEFVkNr+OGiMFUpoBK!g(m-h&|5cj zsDg4*D_oQYJuv`6X6J3;7P)uB0TolFFPw{Ym>4<=#sZp~L(L{OZry(UJ1Nj6d6grBoppQEz zvtMBOahdz-kLlBj8M!ZCg)`y`MGy?j0Gb9pXfTVP=}bk;bdi!9=71ppK)!&ThF0p1 z6nWG-6BgT1aZR-{T&c#4P5*pqZyr z#{$H^j9>lr&oM9Lm!@WFWj-d{0`He52Gvmx zi(_ldJC$QG`mGI2l)=i#59U+#~&62m%X<8E`l1 z1|(oyq_^mi@@Zn>nfYzDTmyWcfhY+k9Q?fC>a2`E4Apes$Hyj?qGB?_f0z`28xG?& z5-QY3S6+#N5>*xIJMJs{kLmJ)JV~BhKnfv<99zSo(q{w#UC=FSmqun9*f5{u&@521 z8QWbeL#*1Rt=s|M07C&g$Qub5P_;8ENbLMXlXsmXugWWRl9M?K#N6k@xzK1XE;Lst z1s3gZgc7)-^zjHVpV13wh6C$BADxt6x~~Au?nj&nOuBI$Y$LKmqGqLcAwkm{$zG&F zDKXVWgFbv5k_(>HvhkqNF%)zTTuiRM#!|OAl2f7qbN3)Ek{-?k@mUdS#qz0% z2U@l(EEZz@sHCm-5vOlyejT?-G^|uX2?AVPQd|ru<9YQfrgwtJeJQT|RQ*p^(l59I zQ`IP|0~ewHxySR@U}(WTix|DDb&+6D?g)Ymmv@(=ORB4{b&tk2!qg&n?!8;Tb2#je z?S;dnuhf$Qhs-Yau5X{;>)Fft6KT_48kJuRV_E6H-@kvKG5c^*YIor7%0Pfhv~4;Y z{Yq}d?E70k?|84v!rIGbO_du!R^=c`12P?5L(sZZAx=QR0ayXM$h2$~6#!)m)S*xj z4oIN6=>6l!_V$tMU}AQyAd!&VOE3^54DHW4Iu5N|-CquZ791$9#rhxJ`tRSjt#bU^ zK@xz!dZE>3;^fJbLan^g+;$!BL+$C3?ZaYv>7;d3f1-rGQPFZt@CDH_z(5eiyx>P| zGJX_#bk6#8`wtF`X%vjL$XBX$hC*%Y!`6{HbX*4|&PZfYA>GCmyuZ0LuOlAh%@V{( z8kg1hPhqWitj+{gIP!sjbC@C^K#`SxlnX~WI%-8aBFt&{MKtJK^4=>NibR-*b|8q= z3lM*k%`vgEe3p>F7B8V@ncFLFVu{z}A+mJ{YGC3hZZO8cp*+Shkn zi{f9rxLjr8h>;hS_nlaH5Uy}jcBu5D(82c7>4*0QN8Ek+4i`ab$W+G(HboA}q)%Vk zjD^7{pq`p8CdO4oUR)lfDGG~Ws{A4+qY<%~Pw`5NptgtD9?pV@#Rs??am>uH$q6Px zupei!-kG}F(s1f3hEK7tr7HZ#12&kh2V4MN{)I93_|z#8_$@f$Sr;S_p69^j(aUeq z#t9Lz)oOk)D3PA!YdeL7zN37lV(oCPaKM#`*E$&USf?im!?j!i%R;2}n^L->=A!?X zRNPdq)Y2Hb(QQ)~ncwkm^x$`jPAFH*$Dvhib8ePVT){f1_$&D*sOn!@{PQ*(ramWH?zQg-d|6JGe<1}I)e+N zemLGoKU0J6^!Yp0NZl+-`d9(WuOQ|L5^wG?36OJ#9)^)(FqvHMKb(PAXz< z7YG71nK%5Z`fw>s1OeVvf6_4vww1~Cu>A4vR-d@42V=(ONA+!9vK}GyePchMg568h%|DGA&~BZ*dpb9nri>C zqYM+}H(>}ui7E#a%P6}Pm6(Om;)i*I@{>5V7_K~=j?T4K)mE{vgd@_23nJ{i-bS3x z?2;~w7h9BQBPC!$bnqNwLs;Ghz+%B#VF`cU0DIWP?5*ETKxfkk0xYWR(3m!u+;=+p z^;*Nk=Qw_YIEhFa&Fp54K5&TJ2SpbE51BDo^jrE6zwMXu)S9{r-DYP=|K)9-fg z%k`>CS|~O!)RrfHl*)0qL#+fW2Zx!jvS=I@GZUvjAl^48L8A6rGtX2)C0@1EVHrH# zuKz|~s8xerSWGnj!%2?_M8@meWK*~exzDwtk1abOfZBVf-zOc>_W(^h0JjiEaxFxR zGIX3h9T8m-+F#^}uuvv`4Q&T(UZ-;D7iv zT($|6s%C_<^BdHb!vGX(9oq|$>q%;#_VjkCmXVTbu^ai@{C9lGJ~XtXHYCkus#0wB zk^`62Od5Jtvrd(RW&NmD+TBXE(1EGP)OGAQr&eZxpw!_m7!mYMU^hmQiL~; z#@DtPe0C!ZK~3urbXWoK*}~J$lB_bP&4A4FH9Pua7^a=)pw@xu#-J4tD2dPf_A{jL zGXa(g|D-s-1{snsFRjRgVnP8B(xapv@nHS4vyGpdxb02`*5|S%g^^t54}KRUs9n9$Wg&I6#f*S zv*<$Qsh>$i(Xmq%I1s2&-*vVBdpoBEJbctkHtbcR0uc_Qwl3U_K(G|K@$%8MshVJt zIUk@}*$_7f2jC6(Fn9pbWHWtilN0F<5R*Tk_A5iH?_za1^9TrBlfp<(UOu=T@>1u^ z-2$mTsZRrUeTA*XzrLhY$mT|4sjz6Smk{4sFVX(bvtEXt!9sxeq6;2P|8~}6rYF%W ziuBgTP|w|pmxjjyypu|tRaHU+@kgq zm5St+QA$Zni-*Uv2B5K@E4En{EWjw{_siFn4EuRw3P6{X)Tg+G`wpHqxU84^qPKIV9 z&6-QQ63XFAd>#Zn_UrTN*@VJUTvb>X5@X;f9^26soPK<46 z_`a`iy}5B_Qc7l0pwY_`Ya@(j zqJOyVnXJrzj+>t_h}eFME0vv^I_vduo~$EB824)zM;@Qc&o*3d!>r>J;BYmmua-I{ z&97ov{+wq=t6u9tM=M*q&ZywGHd2?ImX;)HYkci3?p;uI9;2x(X|yGQ++qrPhBw*i z^viP9A_0DEOo%vQm%g3LtftpS%zYP=r6+&>O`l%9-r27kbLw$|_+{8W?AYFF+_!;; zOHfpB;ooc_E>SKr`szdW!hQFvj?GM`7g(pS4%?Yob#^y~zb3OISbXJM?eyDanE`a0 zMd#c1&tlbB6-g_v5a!UH&`@iZpthPJlflo&Dk(L6G33znHNpE) z+TmH5rLxJgDj9T!==v^>&C~k&zp=^gSbeYlGv_ns`Y=!7;J3F6?8oIXREh-KD$Pk> zLthfw`z197BSAz@z#CS>_3|9`g>^ZS`Q z2w>kL6fKHKGqBEv`~tc# z$Bo4a_bz;L;K8Pppd%uJoYp6vm!8GxwMowG^=o^gW>*q0bEXJwNc;+d%bQ1rS{2o{h*W?m5bA zjV;raSes$l@vZmjm&!d}gP-1vjMn+o4^EBf57*{9NGw~XoY-*6T>(^-oO<;f_m4rY zHN|wBC^X1L@54lR@8Hm6Z+jPMFrL5j(+!TzqeFS-pcxIv^R4pDPn|xkg>02XrJ!JD zT%7t~Z8vso19tr$n%d5M^ZK0ulRM1#pPQ$-8HS8T)%#6sbLK@x(vFnqzn1FVa-r!S zSR;6REBm2wR{p;mvvy`0tqpk9#Ma5&XLGaB&MLaxfNk-T%@xMxIx3R-!k13wTivu} z+A>#;T|2+%#cuN}Yn4}|Tu^dxJ+*;Lc!RL}{#dn>!ui45+Vy)b4(iX)3@Nsn&FZLs zh7907S_&XcKhBV8$V{gX$yGB%ncDbTPjvqdrg+9Hj?Av&p87{_cFJZBZ!%cHY+d5N zf)i*A#tbeS?q<`WiCg)0sCtoldY)fPO=hdZxad2%O$9p~!o1s}b|~|4XDwFeV~DbJ zn31fXFB|QRn4#>D(!OA{J0iqDS)3)FLXOHMPK_k?jE@YK#5OHHaPi_rDr~}8l29q^ zIMhFD18G@Fy0Mj5KvU#<->-PHnHi9t3_q2%o*m+Ea%jcH3!H?5vR~271!qR=a}MSn zoGrKwMy|~@53Q3qp*1#saC?r(-~`^VjGCLCyDX&bi?~}uLM6xGJB@8Bz`fRlZ&O&j zO8v*syyA0q2feuLY|wFjW@>&FjzBVPWXcr{m=X?QoR6z7UvD_vh4Lc# zLuJ)AJ$!SrOP->8r223ZR|MDGxmq$>iXpr^v9-5KigW-)qk!d79vcXFtd#PQ6?}Q& zaNP6X8&x@aT$?rG8kYY(^!P$YR@_k4g(Dy1c=x$hKmVpwP7|;GdjqZNnfixb@Icua zJ=gu`@N_t;In8@q)UZ^_!=L*J><5EaO2V$hZO9XtvGm8{7u?)0d}qsSopV*(=9Rdq z%$8=w}dHly4$>Q+~ zUt^X%{CQkB*F1WWfBFKERZo|jYe#Lj^}V~(!rp&(v?97UUfFm2AM-UA*pbJ>w(SY2 zXDcz@n6H05QOzINl(x2bVW!o}S*tx6s z-Cw>0jSx_P-+8jruJe>eQ{L^j=rEPcReXzMQBwe`9uroh*AR@Ppo6 z*IioonT<_1@^|iXcxT>dNvr2$MHz=}+q!U}f#E&}eyzV7AMO)0gm%+ryX;n;1NBd* zkD@4z1AjlZJnxwvnAY-Zdk{ZSfC{Ep(-p2+zi#5yI@0`hpF>vvNwRm+f6-_7B0Lu? ztI1KdoCBgg)7_pF{lG^Y#yMafT2qaIcBWPOEw-tBf45la(>;YJxvJj351rpQA6uxx z91#OfoQ26&c(xBu1H_rZ?Cqkn7^|4tb^^25N9M6ZW@ayImCCDTD6RiTL-U-3_1d_|C)IIWA7ACT&b8;;*sG%W{wYnI z=HzmbmlH}bth7&EL&F9etJn=d70!Ct&!wPyOy+eK(&v-6g2ppBlJ@#VH8q1q$qO}biv1zaMEr$G<%R23O1{C=WY>$PUz-1yq>ZXi2NDwXbk>>6PhKxE$Q~@cQ0=WM%Axn}6xV zzU5Jg8u6M`>fr9ZTN1{F8kL;xM!DY0GtFkfC9PzpvJGr^u=L`9-#3WDNBa^S&r+6k z@8kGrG50Vx@i4zIv>zVqxzFY^*eW8jaA%0`JZ1=eB^ivcla71^ErTw#{f%7K+${e4 zO-Hi~1n+!ZEx2=5y5CI7+1a2~wXk4?iw;~iT4x6rTib({dMdnq0ricM(z|P8}%7)j~Qs}B7waK(!j%#C{ zgvAzm^uJp#q>5G;+%)3l*H%jVeLUN^hZ*2~S&`}hSdQSMHg?It74*u;o7DfJZA zbia_W3OBA4!YiIX+@uE0KY zd!t33xobA$FiIGLF(GP*Mp0yCn&o1jJNSLq(9qP+U3jst1$n~EtfMCuW(Zjxy}#{m zp@_)HZCdPnRX+c#>3nRXB|9>%FT2j8;Xy&T*41VRH-7Acw_>^}qxiwobgo8dR zR~T=Y`TT#gn{?+Kd-vst9Zs7b6@Be%bYD)yfRWFU51IW}zaxiETx>_d!`4u6t0UOu&W|Gt$-8Se7e>KbK}zG4YWO@*haJ1wH2Y( zvsN2OWYTl{4%ilOyeb&{uhZaYR@2$Ffj2{PlR{j!Gn3w~zRbKpuMP>bVw6O)mDODX z_if^_l%C06nzf#Rn+V#v>iXi;0M<6q;O(N#L)gV0#j3!isDE!p(gxFGZj&KUnfX=6 zvep+UN0H%IUj3I2OH0wTc~-htw;}Zo!aQ&iOR11_Q)LH3e6H+`MJYqyEjmB!i81}P z*}YHU`g4VwTXPI6m1b!-3*F>;8oblcQKUM=&nhL|FZpJOAybJ>f3MR%gjN|*VkcYi z+RuAjOMfO#3Lg-BGE~m-$cBfKmGW3ea!GAke^*$om^0VTS?%yv9v&yrl04t#9-G{I z<|xH(w3{6*v1a$iw)^lN&G>`ASn-?r_8q8m^u77;6!4>g6{2IDfdUXmXcCf-X`;RHGYQAQ!TDgA~ z^Qv%L{Q~`|__Ff$iHyW#71)K#BfEHqrimM+eP+KlQxIQC_e3;Cu zd85~Kz?6>PWUmsL_tO4S=%NVzJ4LY%iuK$rIC>ntwBq82U+-=?rKHkdqa`bBt$b=C ze5zq;OuVC~HGZv%bsdMs5{DYOFJ-P8OSt1lH%NW^?BjJhBj=#PikbUKFv1X3;bv}Q zipV&wKEhr-k1erZyV;=ox?i$m2K_emf3ogvUHd8<`#FCL^P4-oSj^UCDhb_W`qP!8 z?%Yxai$tFWAm?bp7K|iD^Rk`DHz>9E^Rm|BZNYM4TnZ$8p?*H z5dIuN1GBT}#qTlt<+dlbmfotXSmys*qsI`XblhI19t~1Pe%{s$V@D!Q^Z4u|I|SB; zvLQlm5?H^?HkQzKM&0tmzVKxaPk(7h@cOqp!RK?*9{0sr<_eiKU$a#J=YT%z%2b?H-@`S7RaD+b*~%dgG4W4?ACbeUPpB&#rGC|;!f6~p~9@Wb9IC+ zEk|+-&7tD=!AC*2QR-<|6)<1y0V*xXq#2Zf(Iv+_=BQ`o2L?I6Q{)TcMN2VBp@^w1 zDpNwk*512ANzd7Hzh8#?=$5&4ecg~SRr{-sb?UureOJto=jlG7o;0xs&R@=#{2mp= z0JZjy;R!woWPpgXvv0BMuYZ{d6T{VnhcGMuUHuHnKFGD8{=pz=t|d9uFS(0}E|!+N zv1N9?J&JY5c#UzU=rmi`y5}`<=Caf%Y7)@sHq&Hk_*Hq#$wFwiInzNWhr5KK(TFM7ps}3ftjC`oDq4nJknCGqdP#hRAqUVq@d~X#8_U=0E>O;|V8b8fdF0u|u_Zm{jXX zi~!*9;oWKZO*gj*-^_V=6k8XD%#Vuqdd~sr2q%^z>OPk6?c69Y6%p6yaroS2F%#}K z8T6s8%0mwwe7-81SjcqgxfENw0?p(T@&Pp&0losNGYk^ z?|{O$uXDGrBWX5QcTo0Yz4b$MsbOzCFXq0pzF2Xe(oQ=^T2a1S-eDiM_$hEZO#Ao!v>#rs|f9O#&beuLDC$H2o`JsXNx(!<`Q++s+ z^-taS!s#sPx86s^*{u&IJLl&d9Dfr~v!V2i@P6n0^STcdiXANW_9qGswj$++O(&8g zC8gi*SBB$I)t9kl#AR>wZ2ZU(PdL}Bp5Q%t?BQLo73HXd*~saTh_w{kdxC;@MiYWo zUcR=gwb(uT`?qi7-Ql=|n4#al51q*3=he10cy|0X`?Z0rIX~0vHG8&8{0-I}*Q~GJ zZGnsFuJ<_W&zBuPUv~0*`yQ?PK`*@XUzuh-Oi4*ywlwR0-?=X<{~A3sy5)Y~{;@66 zWB-))<St^-lIF zcC}ynqQ%8xruOg6=i3|YBS}YUYIYCx0N+p2W=KYl3|9&UIs(&CL`1g#0JISV*<0fcZCu=QK1S%yzIs_WOgJmVx-w^` z$m;h7ttPXyx8)d$EWva$W^3tYgiBIqlX1K^NE)Gz)FOF5Yuv++!y(0kC*d&=uqUzM z+d!!zSQM&Xe15xrE)5dF~ z41xc{*Uit=!+?h(w0#Tr{+WX9C4TOrLwWVqc?Qe^M)X>y?H;C}Ac(XvN$?|{tJTA1&1!IMwHHKBGs}_|tXcGf|3QjbI;I+=9T*yK4glGN>7(Ek)ML#T}j-*~)#>6uiU& zYTZttS7f1xh`H!_nUrAh^D@yoi*RW=c7{wFQTM+0wnco&0tjUzgO)4T@Gas3{t7-3 zs737!a59*8XK(%Kjg^E#!#APQqyRBq&tEFmS6L;(SdHOWvc<0Q{%L4L|FY}-ZcyEl z`CVJeX{(NGT})dtyJoez-I1C=l9NXqx$x3sdqfn<{>`VWRz0M*rn5{ZQbkQ|MQ6gBzDN3PwZTxDhJrL z#n^3)He}{7nb1;%?oKAaY+Ts%z&tzjv;6NHuoWPXzD7Tf=2)I)-Prq~EjuapK+q(= ziu7+DoDLJwz6Z4Wm|Ybpv*43W1ZxY6ADLnq&C<7vlFZ^@e2eR7X|=4d)HQjrZHYQI zkr7(6Sx<17<8279Hx02}Rh#%;ikwngpXb}ra30oyVjdeiY**c8iGvaRm_Q;f9{D)* zsfeG!;V*ye%L8Tg?O%rIFMwrP!kk%EjO{!p<>k+p3oCLStWyy$+BBtk?#EEd7tc4I zlM^HF+k5V2vNMYl-QKxXs=3L&<=MUh9+K{v=oE z=q*v9bI)p)eS7KbOLHqf{=#OKxf|~#*<-dac<iwEms$9g{mCEk@ zS8{=6+Tm;r6FfJUf#Y)5BF@PUeO5;MxsPK)>07s_MoLHDeHCql$y*kUe<{W5T`k`c zIfahZo@CS_p4lsW>DZ0W7#d zsW2mxUh1uv=$wxHZ0@kT6L6Bfd(>$|?DJwWRj&+L`24krS3aV3s;9iH>`>mqCY@vQqz z>G?u}r{OiPu$dLJxO=#*_53ipO?~eyHeX?gPA@O5=*^l{TtVN~3Zm&fC zf@4{h=WJu=pr};J4+ElplndmyoosegZ7uUjjEWL29gOe$A?q_BJ3f*)rEVb?V38{F z5CtRsq`-gt)w&6xf-jDZ(Az-7Lj0sx@F&l-{3?wG&L9U@I?~}UfV01CC%+)at-8eA zLH>A_7LSEP7+B_8Q4}~_3OZt)pW1n7QMNLh`UwcnC{?)HTPjyb9H07Yp z4Vik>`I7fSH6G3u5$#!>dWChGMG_@soyqJ;`)H%j=wK{tIIqo8hBa5ZraH1L#!G&r zfqvzRw8gAQ$=%E7o*FgP%5vlOEHnByN@6LK6b+tSBqnRXCHm~{5go6g`xf(J|9hPy$8>c@+QDM zfG&u4S%CCLjMQ6!At@MAWct=F!Cu+bNut`v&|z~%e(9WXO|R|b(DC3i@1DY`U1%gU zDS6lvWgt^A@^cQ44;}Vd**;vg!J{YM{WAhdm<{LF)u$gmtiU$9jZD^hblRs56W?5u zikUE1w*&7;ud5CeCHEA!PbfrsOpf%R!yjm;gP!5R(!n6r9Vo(XtpT1=k2Y6NQ{zk~GXRfa0w6F6dV{0)7%Yf7n6g@X%`r2_#{4GqH>KXhtr+c( zNheY=UoE`}Y*4H<$+{ryVS)zzo5=1>;W1OQKJ%LS0ZvCv^@De6k{cM_v${Oc5; zp{8ao{y-jzB84WrT5#q3U|nO>*C{C_|4=ssfv4r+on}t zF;!&xZ*j1B?%Y+F?o+z%!aP>Zj#ZL-BA3o(ERW*bvPkPk&T_wSYy$IWNejlUq z`bu_e07e_lYs17j>~(%D>PhH|HaYZx{L6oH2y?g&_h6S6Bqj=RO60I5W{g9Ij!MkHr&y;2w{PFpe)XX3M3DVBkPc|xbfyHk@@aY__(bjUx0vnC?XoInS;mCNyuk7N zzV@C+L+`uLT?doy>i0WzlN#M1UTg*Bn@3=&S?ImXSMDplu|Z#_xQ0+e04eaMq*_HN?lV$Z#1czg~Nv-}Z8; z=y-wd!npfHJHjxgUk&ewS!fPnq=M_?2A{zVWcCtft1M>K#BXwkSB1ZV*@OFb4-a35 z-vk&q&gehxg>CI)q$7;_v~_?=@EEW-?Al* z0=sFE7het!XQc&S0>o0l_t0owv926;@p2Aj*XX%Oh*wKX1?^V$=R|}8HTVc;l&ayC ziV9t>{8>5j5Bo*AvH7q)ob(SXaIf;n$#-fp>(_C*DQdYgK%NZDD&nJZLA#(OmN zu4tjX*X~Y@f-ll9n?IGQMyl-*3g0divm#hO@@%iSwG7hhV()~m*! z*a(FDLJlW#2KDSQOQs6LQjceO^_^?)B6WzQ2+%b9jbq_~-uAMFhH*}V;^mcD>m3_) zlB51Uv8ccdz(|qQC1w3fka8jW8Xzy^#NrJZxZU*p0gO~t*((g-B1{2Cg8?y+=Rsq+ zfRi_5wj4_pQAi>BVgzi#vgRpAX;VrPn-+|p!Pu*s6FZA8Gb=(&M*TC?!6~X(2;-w+Kw`qBM zA@_rUClVI5?Cz8z?h+*T?W*8Gd>83)F!J7W+vc4@#g2{5b}=U$^XgQ^a-MG&DrmL^ z(c0Jo?V)}GW`WbJ&~Y3++ASsZSvuz)0EGBSih9)NK3AZAH}Bqaz$}{Qlu?}u`=fBq zp%tArkKG`Le?TNa$>EUTx9j(`pTIArtf>8cbc}v)_Q@F=e0xrm@oLQRn)g8N%WUV@zt?xn zc$@cVrt#JRrIyoK>)l|w1mtlQXkr*?3i^!e3XmpJh|sc+t;>Dx2~&kZ$Dc^JO1TK) z7mB6(mq&Um#3d717pRAWx`3ku!2a(+!UEQ@mk2o-By1cn3tgG}N0B>^+}N0I|Z-BUtiPr>66Ke;odwPp2e^*n^IM59Ku*IM`C`ix@pp zr6jU=YggQ66-%U~9CZp*0(MHB0D>WtCxOne?|bH12sf-Hc9Hi8=AC{Le+&bi4-X4e zG?(sAmp&y(f~jEr8z&YKRUrnuEat?rXgFJdokl2Db9eF*geq|QKCWlaj%qp;woDCz zzK#XSN@H_Wc;Lw#NxrQ7%`IgudUvu=PVPju@8*uh zFwmKrv%TCBdK%tNDzzlezRNx5@s=wEv!1!VU9<^kP_G&Z*7hGb0w(Lg%@7)>ho4X= zSJi*28Ury?68HjHUD|CG1H+}XZf?uBNRpHS`o^CCza-&ma}|;HPM9MlSR_G+YuCXq zQepG7jH4>TrJxBp&+ihDLfE;iic$pFyd!{*I$;QoueNbxGCVE3GV){j#$cO8&;Hu0 z5dt}bj}tNeuQMg-53*N8!gNqF4u)J!8z+QCaUx~-FZ6Psa5&!AO7*B`pdCrb5U)}mP1?@3$nT1Ajikwp;B;o_Mw@FW^dv5s{OtAfz_C=B!Vu& ziwN{hThNMNkZ>wX*rE}1Vp+fmpaFT?BKelLpDdw0pX7-!Jb@jMJ+KrcnvxKKQo^O) z+Qm@U3{HkITdv}Q+ra2g6Tj((`yseO3m}d3puW1H`E+%$SfmAM-F8poW3>fwB%Aj<}tlbcLh+6pr#H}@1f=4rCkf^X` z5tV}$a7BF)*c)+1B$1!Xa&QBl z5(VmeKm?E&%X~dgXWW`_Gx!s@0och!N$=sZgoK1icQT89egE}H`^ijKFE9D6zK&u# z4jrpH23x*^f8-S$o^{zb|GU+>hTrpeTRN=^%t$DuMfK9Phsr-kZWoXxU=@q*clKC) zs5`mR`B+MfQ%CTcOEjBY3+0afmQI+B>;H`94U>k^SWQ#LX-*0s3!^6F16-PTM3Uni zSKMud+#d)Ryd)xG;3-sW+H!!Wm}`e1*gn=G@-5*s(15ykj;daRvF#tx*vV_8?y#N2 zE-KlL1|5hyCB1%dHU5=@sktP5>=sRk39=dYC|ml7G!z2g1p)%3`zL}Cd_!hHVN?zP ztOJKnUmvghpBRzw338!-vCI~AB=H`<3M@r(}a|EEsnNO>x2GJja5=7hxjtRMk z^8{I>Ul9HL1O;f=5}Nb*XB|JyWV$w{!c3l=9$-ouj^`ZnpD zqgnk==db%g>XK2+A86U(nm8tb%^%2ym=U}T2tjwPC^o4PX~ES{u^S1aPmxcP%Zkp_nkZn3)_a8GXXhwR52n->??oJT;RRrgxNU8t{ou~>a zDPYGbxX*YPAIUL;*F&pLCfF0sL0*m^P!DL+qJm&v2#t#Ou?!IvDa3Pl+#uiz1jP#@ zKiF(bWR4&J%LL}}Ns2&IE<#8oLCDGalF@%N|4X+4C+=N=W7#Q0@EerOOz;9?4D$UK zywTwQXHfHr!^MEHAVS~(3O&T?NJ4-zrc2<92)2n}M=n~y|MuTU5m&eq&JUIhy+3Co z1J;N)hVP36@gPz9qQ0`JE!engb!|8WrP-)+Fi?8S+{g#=B;bLYC>ZH=FRSaD%Jiv> z-|&9sf{9fe!Id?8v zKCl~GrIWp@!Xfey7ucYvU;~%`cgL(*3$yY&=8$*-j_${Df?F~Ffh4gUM9;vv6R;5E zP>@*DK=z;bIO5;-AnYKKIB{-}C*__XL?}2681_S?=tk15(h5>g1DF@Y;#2|Y} z%!owTR9sh0L`?++u;-mZnvS5lNUggS6P5mH@WofsL&<8i#fJ|QaiK>^+;4SbChI~9q; zp<&}ka?v#TAUx$D0yip87EYXY7?=pM!bsRidz%vP62z-32YVsp?K4Tl)LTP${x~Fx zD&a?zyAPADM?e9HqtY>yO)2Qa?GlQa&e4J#{4pq`Zo8n4#0%h_{mCCAP4E`?EDsdj zwJO*GByR;a&;$v!M`on3aF<;!;_2yxCKZ9f!WUvA4+BfXK=sK^6?p!9lJ9_S@52*- z-S<9I=PzG)TCHnI57_kBVvF0Dn?&2AG2u65!<9b$8UFr0^S#1%UDX)eF@$}&f3454 zJkQVqV*)KbR!JQS3u@4==#lDde0oXD!>pcc6pIUS9?SXvUXBW1H4_)NS7o*}~8L!ve~sxMN00Vfcc5*d&<94I)= zz`}*_yPntK+K4+R@TajB?@khD5E=zkiGMdOE?2xa4Xz$EhzRL261U(Lh^67Z@RkJK zNVWkAu!q=^2Ek_%Pf+O;l71o48?o}PE4Vp=-HlSgb&(a?#LSVZzl%&XDn=-_j|YlM zVvmsuI-v>ms+SO=T_Bo4aFLO4uR~IP-S{$m zJ@=67=|P#snz^1{oK2}e>ihdAZR6 z1vc1z5>p-=j65V6_#xktc4)=|-}6UiVK<0uQKXDbHJ2CK?QJYqY$)#QnP__Bkao`P z`jewGgrv6_i6jTfXs;2GJE>=8Bz~=XH#@yk&DgQ7XJpovJ?!z7ULD-Q(JsNvyKkBA zgRU)&Il5E+Qm9nmij|JDt-tYdao?Qtnf2w+qtYQ&$pcQWW0;x6CVh50Iv-Zi!G zYI8y|lCAciy0hJu{XjB0WN)Q^`awoch5?JADq2lfF!GGV7eo7%a5j=2t2BE_bXe^p z#=%=grwl7?88Xpz3`aUnPG?xLuUFCZtoiWNIm3)$#q{*IjC6_p%?v$Jd{ony?F^`8 zW@yZ%?+>VrN;XPzSx2&AqrbkUOJsVO(ql3VGOjZiO=r5V*Y4ErzOEZk%Q&UiEfsA| z7t4^?Z>Vmp#JuyhmO(BSN}Q!i15@z9~r@Ctm30)gb(9q`}7<6%@VyEU^xaVM~fzkAKXS zpYj`(t)^+JRe78YJ!O=Wk#71XdT-@lRY%I&UGF60-5C;@*BNwGQ89AW%d~7E1xG9} zF6@C=bakpxk}~sPTBkSGKSalNvGqMe^)wev(IH%}#QqEed~*k4fq3Rc&x_X4*4FM; z#zJ3bY*|f7EJZm(kB-GWW5L4rvO3+z3?K!AqEHBmk>wdq6qa||=^ zo;tQ{1959-afk)FfMsT;AH3yaSb17aA{}p}fJ@ep6QxJh;sitC$u=_5v=eQ8Ka$Vi zzJ;E#Fl_F_DMIgfvgu2zZpcIiL>i2=5Nl{gNbN;t<*fn5(t+sg9|y{Q9q<}$OtdR( z$!YJyWy=szTYr7%;_Y3J*C%`D7ADthKbTg2aK33}wumeg)I$t6-%PYhh>?aup23*O ze-C$uHJrcPvcYLUbJOPTrc3JvK7ElpTy{RT{bHm39a`w^nMs7a_vEMVOE8lqeI@b0&v za}aesv6gDh$x~DpPAs~baZbkdrZcIXmgySDa1{A&^7d46A27r0l7V@eKX+(WEO9D% z0(MW$QKKx)z%b|T8iJ!umb7xoj8%xv8PhQ)}fNitV@#L&sa2B1n9tGetnp%T(wo( zc7IL=U`kIkCgVY3NEjeTJUXNE5aV*j!DtAKO;@FdC&LiafdhaxRl12`|8h}!H6>MT z(f!&m4SYn^`lovtWPPp8nFn-C!yjCSm<70_Ww&+v?@sMxGkUdCJEfxQaKo1~9O=L< zVsRl9YSQoOcC#5UQEO59{>n$`WMhEr+))R4hl<;6Ele0`0JzwYkT2LO_<(jMWb2pI z1&~4J&~UP9q5_$CgG>WY5$GOXJHv`X?ERHUYEBKKkpn5MAOo>Tc$kM7tTg}f509}D zJv}`oW4*<3adFOS5)V+ZLWxRUQ?m!Bzp10HkQOB<28{F)&I0(Ls;6uiu)Qd0%1ipJSm;tDHbG0aqhKzCkY5jnP}FERYyXfvaCJDWvJKUz1QGg?4X%l3pfY#45;mbXCqW7E#zDVVMwq6g|nE>JA&r6 z+LK>2CvFolZBI?Mde=Af&21Aoz)2>pl#aFakvg9Jr%(@ZvakzP&L2j45_@oLAFkLo zMY-yTQBfisbGKnSIEjK$ktE!7SS@MPJ{dmMR93kOHr_rdJ2iLHq*(ER!Uwr?SKq3mJK8=sZwVm7rU4zugjd&9+GwNRsluB|D6{W8$9hOQQ{pa>DD!v*}7nxyt zEdb0#RJ1Pl$E!W-NE%Hs1I%);L9?NH%WoyTIPSChnr%`ioh}nigl$wbk6@j%UTWtY zpU)SE+b4gO)jLi7c5?k%cleXYTP|Rz*F?05$Ka>EhuXjYZg_HA|3e?QPt2SveIrwY zmFv7uxx2Uw_*;kim`!M|tsAJm5K~2aGu*>q&=*|frclG68S0#gbM<&TqGcJK*9me# zyK_Q->aTDqbEX$E4A_=y=t%?lV&oV$Gg|j(Yf}yx4hkP&8e_*SOjJebusq6ldAuXu zMPPCC&<)+no!ypDHeiR@BDgcc6yc6S_TnWF2Ur;u(W5WX71$m(7ztP5A2dgIO$9l5 z55|(>U(hsoWfbjM19$KZY=k85$0icmPF7Qb0k77h(?uIW&Klu&L}S!9ZR+~?ktZ64 zlNfRvEP|o28*fF%!;*r9($Ux1ge4Hf+=hF|(5uTOY>;&4A-)O>frKc!m3To?ENDv5 zAAAYK=Mo&L24A$bH6LYW=Titprm8}P-J?;gJ~jpDyH(;0#i?ccbv0ylgoeVb=ai ze})Evg;W^4Xye1Xaq;o}Sx)E4s3^mvTyX-6g)IcHKT%fOmeK=efU&|e&0}Gf@IfJ z$vz#tCKZ@Z1z;g3~IbK�X3mg(EL}t58447; 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 z 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%0ARvA8@iZ1yz?(*sqO77$dDDu%(}n%p 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" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/remote_service_status.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/remote_service_status.json new file mode 100644 index 0000000000000..07dddb5f70f81 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/remote_service_status.json @@ -0,0 +1,3 @@ +{ + "eventStatus": "EXECUTED" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/vehicles_base.json new file mode 100644 index 0000000000000..18d211927e69c --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/vehicles_base.json @@ -0,0 +1,48 @@ +[ + { + "vin": "anonymousMILD_HYBRID", + "mappingInfo": { + "isAssociated": true, + "isLmmEnabled": false, + "mappingStatus": "CONFIRMED", + "isPrimaryUser": true + }, + "appVehicleType": "CONNECTED", + "attributes": { + "lastFetched": "2022-12-21T17:30:40.363Z", + "model": "M340i xDrive", + "year": 2022, + "color": 4284572001, + "brand": "BMW", + "driveTrain": "MILD_HYBRID", + "headUnitType": "MGU", + "headUnitRaw": "HU_MGU", + "hmiVersion": "ID7", + "softwareVersionCurrent": { + "puStep": { + "month": 7, + "year": 22 + }, + "iStep": 558, + "seriesCluster": "S18A" + }, + "softwareVersionExFactory": { + "puStep": { + "month": 3, + "year": 22 + }, + "iStep": 560, + "seriesCluster": "S18A" + }, + "telematicsUnit": "ATM2", + "bodyType": "G21", + "countryOfOrigin": "DE", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + } + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/vehicles_state.json new file mode 100644 index 0000000000000..5c255221aaa9e --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/vehicles_state.json @@ -0,0 +1,264 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2022-12-21T17:31:26.560Z", + "lastUpdatedAt": "2022-12-21T15:41:23Z", + "isLscSupported": true, + "range": 435, + "doorsState": { + "combinedSecurityState": "SECURED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "rear": "CLOSED", + "combinedState": "CLOSED" + }, + "roofState": { + "roofState": "CLOSED", + "roofStateType": "SUN_ROOF" + }, + "tireState": { + "frontLeft": { + "details": { + "dimension": "225/45 R18 95V XL", + "treadDesign": "Winter Contact TS 860 S SSR", + "manufacturer": "Continental", + "manufacturingWeek": 5299, + "isOptimizedForOemBmw": true, + "partNumber": "2471558", + "speedClassification": { + "speedRating": 240, + "atLeast": false + }, + "mountingDate": "2022-10-06T00:00:00.000Z", + "season": 4, + "identificationInProgress": false + }, + "status": { + "currentPressure": 280, + "targetPressure": 290 + } + }, + "frontRight": { + "details": { + "dimension": "225/45 R18 95V XL", + "treadDesign": "Winter Contact TS 860 S SSR", + "manufacturer": "Continental", + "manufacturingWeek": 5299, + "isOptimizedForOemBmw": true, + "partNumber": "2471558", + "speedClassification": { + "speedRating": 240, + "atLeast": false + }, + "mountingDate": "2022-10-06T00:00:00.000Z", + "season": 4, + "identificationInProgress": false + }, + "status": { + "currentPressure": 280, + "targetPressure": 290 + } + }, + "rearLeft": { + "details": { + "dimension": "255/40 R18 99V XL", + "treadDesign": "Winter Contact TS 860 S SSR", + "manufacturer": "Continental", + "manufacturingWeek": 5299, + "isOptimizedForOemBmw": true, + "partNumber": "2471559", + "speedClassification": { + "speedRating": 240, + "atLeast": false + }, + "mountingDate": "2022-10-06T00:00:00.000Z", + "season": 4, + "identificationInProgress": false + }, + "status": { + "currentPressure": 280, + "targetPressure": 290 + } + }, + "rearRight": { + "details": { + "dimension": "255/40 R18 99V XL", + "treadDesign": "Winter Contact TS 860 S SSR", + "manufacturer": "Continental", + "manufacturingWeek": 5299, + "isOptimizedForOemBmw": true, + "partNumber": "2471559", + "speedClassification": { + "speedRating": 240, + "atLeast": false + }, + "mountingDate": "2022-10-06T00:00:00.000Z", + "season": 4, + "identificationInProgress": false + }, + "status": { + "currentPressure": 280, + "targetPressure": 290 + } + } + }, + "location": { + "coordinates": { + "latitude": 1.234, + "longitude": 5.678 + }, + "address": { + "formatted": "Leopoldstraße 25, 80333 München" + }, + "heading": 180 + }, + "currentMileage": 4376, + "climateControlState": { + "activity": "INACTIVE" + }, + "requiredServices": [ + { + "dateTime": "2024-06-01T00:00:00.000Z", + "mileage": 29000, + "type": "OIL", + "status": "OK", + "description": "Next service due after the specified distance or date." + }, + { + "dateTime": "2025-06-01T00:00:00.000Z", + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Next service due by the specified date." + }, + { + "dateTime": "2025-07-01T00:00:00.000Z", + "type": "VEHICLE_TUV", + "status": "OK", + "description": "Next state inspection due by the specified date." + }, + { + "dateTime": "2026-06-01T00:00:00.000Z", + "mileage": 60000, + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Next visual inspection due by specified date or, if shown, when stated distance has been reached." + } + ], + "checkControlMessages": [ + { + "type": "TIRE_PRESSURE", + "severity": "LOW" + }, + { + "type": "ENGINE_OIL", + "severity": "LOW" + } + ], + "combustionFuelLevel": { + "remainingFuelPercent": 65, + "remainingFuelLiters": 34, + "range": 435 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "isDeepSleepModeActive": false, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + }, + "capabilities": { + "a4aType": "NOT_SUPPORTED", + "climateNow": true, + "isClimateTimerSupported": true, + "climateTimerTrigger": "DEPARTURE_TIMER", + "climateFunction": "VENTILATION", + "horn": true, + "isBmwChargingSupported": false, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": false, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": false, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": false, + "isEasyChargeEnabled": false, + "isMiniChargingSupported": false, + "isEvGoChargingSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteEngineStartSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteSoftwareUpgrade": true, + "sendPoi": true, + "speechThirdPartyAlexa": true, + "speechThirdPartyAlexaSDK": false, + "unlock": true, + "vehicleFinder": true, + "vehicleStateSource": "LAST_STATE_CALL", + "isRemoteHistorySupported": true, + "isWifiHotspotServiceSupported": true, + "isNonLscFeatureEnabled": false, + "isSustainabilitySupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "checkSustainabilityDPP": false, + "specialThemeSupport": [], + "isRemoteParkingSupported": false, + "remoteChargingCommands": {}, + "isClimateTimerWeeklyActive": true, + "digitalKey": { + "bookedServicePackage": "NONE", + "state": "NOT_AVAILABLE" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV/vehicles_base.json new file mode 100644 index 0000000000000..9db1bec197d6c --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV/vehicles_base.json @@ -0,0 +1,49 @@ +[ + { + "vin": "anonymousPHEV", + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "mappingStatus": "CONFIRMED", + "isPrimaryUser": true + }, + "appVehicleType": "CONNECTED", + "attributes": { + "lastFetched": "2023-01-16T15:33:53.940Z", + "model": "530e iPerformance", + "year": 2019, + "color": 4282532418, + "brand": "BMW", + "driveTrain": "PLUGIN_HYBRID", + "headUnitType": "NBT_EVO", + "headUnitRaw": "NBTEVO", + "hmiVersion": "ID5", + "softwareVersionCurrent": { + "puStep": { + "month": 3, + "year": 19 + }, + "iStep": 537, + "seriesCluster": "S15A" + }, + "softwareVersionExFactory": { + "puStep": { + "month": 3, + "year": 19 + }, + "iStep": 537, + "seriesCluster": "S15A" + }, + "telematicsUnit": "ATM1", + "bodyType": "G30", + "countryOfOrigin": "DE", + "a4aType": "BLUETOOTH", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + } + } + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV/vehicles_state.json new file mode 100644 index 0000000000000..483ddbc5c2bbc --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV/vehicles_state.json @@ -0,0 +1,223 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2023-01-16T15:33:52.449Z", + "lastUpdatedAt": "2023-01-16T14:52:04Z", + "isLscSupported": true, + "range": 544, + "doorsState": { + "combinedSecurityState": "SECURED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED" + }, + "roofState": { + "roofState": "CLOSED", + "roofStateType": "SUN_ROOF" + }, + "location": { + "coordinates": { + "latitude": 1.1, + "longitude": 2.2 + }, + "address": { + "formatted": "anonymousAddress" + }, + "heading": -1 + }, + "currentMileage": 45343, + "requiredServices": [ + { + "dateTime": "2024-11-01T00:00:00.000Z", + "mileage": 31000, + "type": "OIL", + "status": "OK", + "description": "Nächster Service nach der angegebenen Fahrstrecke oder zum angegebenen Termin." + }, + { + "dateTime": "2026-11-01T00:00:00.000Z", + "mileage": 60000, + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Nächste Sichtprüfung zum angegebenen Termin oder nach der ggf. angegebenen Fahrstrecke." + }, + { + "dateTime": "2024-04-01T00:00:00.000Z", + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Nächster Wechsel spätestens zum angegebenen Termin." + } + ], + "checkControlMessages": [ + { + "type": "ENGINE_OIL", + "severity": "LOW" + } + ], + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "reductionOfChargeCurrent": { + "start": { + "hour": 0, + "minute": 0 + }, + "end": { + "hour": 0, + "minute": 0 + } + }, + "chargingMode": "IMMEDIATE_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "departureTimes": [ + { + "id": 1, + "timeStamp": { + "hour": 22, + "minute": 10 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 2, + "timeStamp": { + "hour": 8, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 3, + "timeStamp": { + "hour": 8, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 4, + "action": "DEACTIVATE", + "timerWeekDays": [] + } + ], + "climatisationOn": true, + "chargingSettings": { + "targetSoc": 100, + "idcc": "NO_ACTION", + "hospitality": "NO_ACTION" + } + }, + "electricChargingState": { + "chargingLevelPercent": 79, + "range": 15, + "isChargerConnected": false, + "chargingConnectionType": "UNKNOWN", + "chargingStatus": "INVALID", + "chargingTarget": 100 + }, + "combustionFuelLevel": { + "remainingFuelLiters": 43, + "range": 544 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + }, + "capabilities": { + "a4aType": "BLUETOOTH", + "climateNow": true, + "climateFunction": "AIR_CONDITIONING", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": true, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": true, + "isEasyChargeEnabled": false, + "isMiniChargingSupported": false, + "isEvGoChargingSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteEngineStartSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": true, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remote360": true, + "sendPoi": true, + "speechThirdPartyAlexa": true, + "speechThirdPartyAlexaSDK": false, + "unlock": true, + "vehicleFinder": true, + "vehicleStateSource": "LAST_STATE_CALL", + "isRemoteHistorySupported": true, + "isWifiHotspotServiceSupported": true, + "isNonLscFeatureEnabled": false, + "isSustainabilitySupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "checkSustainabilityDPP": false, + "specialThemeSupport": [], + "isRemoteParkingSupported": false, + "remoteChargingCommands": null, + "isClimateTimerWeeklyActive": false, + "digitalKey": { + "bookedServicePackage": "NONE", + "state": "NOT_AVAILABLE" + } + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/charging_sessions.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/charging_sessions.json new file mode 100644 index 0000000000000..568da14301c4b --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/charging_sessions.json @@ -0,0 +1,50 @@ +{ + "paginationInfo": {}, + "chargingSessions": { + "total": "~ 16 kWh", + "numberOfSessions": "4", + "chargingListState": "HAS_SESSIONS", + "sessions": [ + { + "id": "2023-02-01T15:51:30Z_213a0c9f", + "title": "Yesterday 17:51", + "subtitle": "anonymousAddress • 2h 16min • 0 EUR", + "energyCharged": "~ 5 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-02-01T13:00:12Z_213a0c9f", + "title": "Yesterday 15:00", + "subtitle": "anonymousAddress • 1h 02min • ~ 0,45 EUR", + "energyCharged": "~ 3 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-02-01T03:27:56Z_213a0c9f", + "title": "Yesterday 5:27", + "subtitle": "anonymousAddress • 2h 42min • 0 EUR", + "energyCharged": "~ 6 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + }, + { + "id": "2023-02-01T03:10:29Z_213a0c9f", + "title": "Yesterday 5:10", + "subtitle": "anonymousAddress • 17 min • 0 EUR", + "energyCharged": "\u003c 2 kWh", + "sessionStatus": "FINISHED", + "isPublic": false + } + ], + "costsGroupedByCurrency": [ + "~ 0.45 EUR" + ] + }, + "datePicker": { + "startDate": "2021-01-05T09:03:35Z", + "selectedDate": "2023-02-01T15:51:30Z", + "endDate": "2023-02-02T03:32:19Z" + } + } \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/charging_statistics.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/charging_statistics.json new file mode 100644 index 0000000000000..c4e2e691913d7 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/charging_statistics.json @@ -0,0 +1,11 @@ +{ + "description": "February 2023", + "optStateType": "OPT_IN_WITH_SESSIONS", + "statistics": { + "totalEnergyCharged": 16, + "totalEnergyChargedSemantics": "Charged a total of approximately 16 kilowatt-hours", + "symbol": "~", + "numberOfChargingSessions": 4, + "numberOfChargingSessionsSemantics": "4 charging sessions" + } + } \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/vehicles_base.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/vehicles_base.json new file mode 100644 index 0000000000000..fc75bbaab49f0 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/vehicles_base.json @@ -0,0 +1,49 @@ +[ + { + "vin": "anonymousPHEV2", + "mappingInfo": { + "isAssociated": true, + "isLmmEnabled": false, + "mappingStatus": "CONFIRMED", + "isPrimaryUser": true + }, + "appVehicleType": "CONNECTED", + "attributes": { + "lastFetched": "2023-02-02T03:32:13.686Z", + "model": "330e xDrive", + "year": 2020, + "color": 4284572518, + "brand": "BMW", + "driveTrain": "PLUGIN_HYBRID", + "headUnitType": "MGU", + "headUnitRaw": "HU_MGU", + "hmiVersion": "ID7", + "softwareVersionCurrent": { + "puStep": { + "month": 7, + "year": 21 + }, + "iStep": 550, + "seriesCluster": "S18A" + }, + "softwareVersionExFactory": { + "puStep": { + "month": 7, + "year": 20 + }, + "iStep": 554, + "seriesCluster": "S18A" + }, + "telematicsUnit": "ATM2", + "bodyType": "G21", + "countryOfOrigin": "FI", + "a4aType": "NOT_SUPPORTED", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "androidStoreUrl": "https://play.google.com/store/apps/details?id\u003dcom.bmwgroup.driversguide.row", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt\u003d8" + } + } + } + ] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/vehicles_state.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/vehicles_state.json new file mode 100644 index 0000000000000..059a406b89a9c --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/PHEV2/vehicles_state.json @@ -0,0 +1,256 @@ +{ + "state": { + "isLeftSteering": true, + "lastFetched": "2023-02-02T03:32:14.049Z", + "lastUpdatedAt": "2023-02-02T03:31:31Z", + "isLscSupported": true, + "range": 290, + "doorsState": { + "combinedSecurityState": "SECURED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "rear": "CLOSED", + "combinedState": "CLOSED" + }, + "tireState": { + "frontLeft": { + "details": { + "identificationInProgress": false + }, + "status": { + "currentPressure": 290, + "targetPressure": 230 + } + }, + "frontRight": { + "details": { + "identificationInProgress": false + }, + "status": { + "currentPressure": 290, + "targetPressure": 230 + } + }, + "rearLeft": { + "details": { + "identificationInProgress": false + }, + "status": { + "currentPressure": 280, + "targetPressure": 280 + } + }, + "rearRight": { + "details": { + "identificationInProgress": false + }, + "status": { + "currentPressure": 280, + "targetPressure": 280 + } + } + }, + "currentMileage": 28878, + "climateControlState": { + "activity": "INACTIVE" + }, + "requiredServices": [ + { + "dateTime": "2023-10-01T00:00:00.000Z", + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Next service due by the specified date." + }, + { + "dateTime": "2024-10-01T00:00:00.000Z", + "mileage": 32000, + "type": "OIL", + "status": "OK", + "description": "Next service due after the specified distance or date." + }, + { + "dateTime": "2024-10-01T00:00:00.000Z", + "mileage": 32000, + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Next visual inspection due by specified date or, if shown, when stated distance has been reached." + } + ], + "checkControlMessages": [ + { + "type": "TIRE_PRESSURE", + "severity": "LOW" + }, + { + "type": "ENGINE_OIL", + "severity": "LOW" + } + ], + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "reductionOfChargeCurrent": { + "start": { + "hour": 0, + "minute": 0 + }, + "end": { + "hour": 0, + "minute": 0 + } + }, + "chargingMode": "IMMEDIATE_CHARGING", + "chargingPreference": "NO_PRESELECTION", + "departureTimes": [ + { + "id": 1, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 2, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 3, + "timeStamp": { + "hour": 0, + "minute": 0 + }, + "action": "DEACTIVATE", + "timerWeekDays": [] + }, + { + "id": 4, + "action": "DEACTIVATE", + "timerWeekDays": [] + } + ], + "climatisationOn": false, + "chargingSettings": { + "targetSoc": 100, + "idcc": "NO_ACTION", + "hospitality": "NO_ACTION" + } + }, + "electricChargingState": { + "chargingLevelPercent": 57, + "remainingChargingMinutes": 177, + "range": 19, + "isChargerConnected": true, + "chargingConnectionType": "UNKNOWN", + "chargingStatus": "CHARGING", + "chargingTarget": 100 + }, + "combustionFuelLevel": { + "remainingFuelPercent": 56, + "remainingFuelLiters": 20, + "range": 290 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "isDeepSleepModeActive": false, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ] + }, + "capabilities": { + "a4aType": "NOT_SUPPORTED", + "climateNow": true, + "climateFunction": "AIR_CONDITIONING", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": true, + "isEasyChargeEnabled": false, + "isMiniChargingSupported": false, + "isEvGoChargingSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteEngineStartSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteSoftwareUpgrade": true, + "sendPoi": true, + "unlock": true, + "vehicleFinder": true, + "vehicleStateSource": "LAST_STATE_CALL", + "isRemoteHistorySupported": true, + "isWifiHotspotServiceSupported": false, + "isNonLscFeatureEnabled": false, + "isSustainabilitySupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "checkSustainabilityDPP": false, + "specialThemeSupport": [], + "isRemoteParkingSupported": false, + "remoteChargingCommands": {}, + "isClimateTimerWeeklyActive": false, + "digitalKey": { + "bookedServicePackage": "NONE", + "state": "NOT_AVAILABLE" + } + } + } \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/anonymous-raw.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/anonymous-raw.json deleted file mode 100644 index 5f092beee9bdf..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/anonymous-raw.json +++ /dev/null @@ -1,386 +0,0 @@ - { - "vin": "anonymous", - "model": "i3 94 (+ REX)", - "year": 2017, - "brand": "BMW", - "headUnit": "ID5", - "isLscSupported": true, - "driveTrain": "ELECTRIC", - "puStep": "0321", - "iStep": "I001-21-03-530", - "telematicsUnit": "TCB1", - "hmiVersion": "ID4", - "bodyType": "I01", - "a4aType": "USB_ONLY", - "exFactoryPUStep": "0717", - "exFactoryILevel": "I001-17-07-500", - "capabilities": { - "isRemoteServicesBookingRequired": false, - "isRemoteServicesActivationRequired": false, - "isRemoteHistorySupported": true, - "canRemoteHistoryBeDeleted": false, - "isChargingHistorySupported": true, - "isScanAndChargeSupported": true, - "isDCSContractManagementSupported": true, - "isBmwChargingSupported": true, - "isMiniChargingSupported": false, - "isChargeNowForBusinessSupported": true, - "isDataPrivacyEnabled": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingTargetSocEnable": false, - "isChargingLoudnessEnable": false, - "isChargingSettingsEnabled": false, - "isChargingHospitalityEnabled": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isCustomerEsimSupported": false, - "isCarSharingSupported": false, - "isEasyChargeSupported": false, - "lock": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "unlock": { - "isEnabled": true, - "isPinAuthenticationRequired": true, - "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "lights": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern." - }, - "horn": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern." - }, - "vehicleFinder": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern." - }, - "sendPoi": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern." - }, - "climateNow": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern." - } - }, - "properties": { - "lastUpdatedAt": "2022-01-03T18:54:57Z", - "inMotion": false, - "areDoorsLocked": true, - "originCountryISO": "DE", - "areDoorsClosed": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "windows": { - "driverFront": "CLOSED", - "passengerFront": "CLOSED" - }, - "trunk": "CLOSED", - "hood": "CLOSED", - "moonroof": "CLOSED" - }, - "isServiceRequired": false, - "fuelLevel": { - "value": 7, - "units": "LITERS" - }, - "chargingState": { - "chargePercentage": 47, - "state": "NOT_CHARGING", - "type": "NOT_AVAILABLE", - "isChargerConnected": false - }, - "combustionRange": { - "chargePercentage": 0, - "distance": { - "value": 96, - "units": "KILOMETERS" - } - }, - "combinedRange": { - "chargePercentage": 0, - "distance": { - "value": 96, - "units": "KILOMETERS" - } - }, - "electricRange": { - "chargePercentage": 0, - "distance": { - "value": 78, - "units": "KILOMETERS" - } - }, - "electricRangeAndStatus": { - "chargePercentage": 47, - "distance": { - "value": 78, - "units": "KILOMETERS" - } - }, - "checkControlMessages": [], - "serviceRequired": [ - { - "type": "BRAKE_FLUID", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_CHECK", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "OIL", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_TUV", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - } - ], - "vehicleLocation": { - "coordinates": { - "latitude": 1.234, - "longitude": 9.876 - }, - "address": { - "formatted": "anonymous" - }, - "heading": 39 - } - }, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "status": { - "lastUpdatedAt": "2022-01-03T18:54:57Z", - "currentMileage": { - "mileage": 32179, - "units": "km", - "formattedMileage": "32.179" - }, - "issues": null, - "doorsGeneralState": "Verriegelt", - "checkControlMessagesGeneralState": "Keine Probleme", - "doorsAndWindows": [ - { - "iconId": 59757, - "title": "Verriegelungsstatus", - "state": "Verriegelt", - "criticalness": "nonCritical" - }, - { - "iconId": 59722, - "title": "Alle Türen", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59725, - "title": "Alle Fenster", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59706, - "title": "Frontklappe", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59704, - "title": "Gepäckraum", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59705, - "title": "Glasdach", - "state": "Geschlossen", - "criticalness": "nonCritical" - } - ], - "checkControlMessages": [], - "requiredServices": [ - { - "id": "BrakeFluid", - "title": "Bremsflüssigkeit", - "iconId": 60223, - "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleCheck", - "title": "Fahrzeug-Check", - "iconId": 60215, - "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "Oil", - "title": "Motoröl", - "iconId": 60197, - "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleAdmissionTest", - "title": "Fahrzeuginspektion (HU)", - "iconId": 60111, - "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - } - ], - "fuelIndicators": [ - { - "mainBarValue": 47, - "rangeUnits": "km", - "rangeValue": "78", - "levelUnits": "%", - "levelValue": "47", - "secondaryBarValue": 0, - "infoIconId": 59694, - "rangeIconId": 59683, - "levelIconId": 59694, - "showsBar": true, - "showBarGoal": false, - "infoLabel": "Ladezustand", - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingStatusType": "DEFAULT", - "chargingStatusIndicatorType": "DEFAULT" - }, - { - "mainBarValue": 0, - "rangeUnits": "km", - "rangeValue": "174", - "secondaryBarValue": 0, - "infoIconId": 59691, - "rangeIconId": 59691, - "levelIconId": 0, - "showsBar": false, - "showBarGoal": false, - "infoLabel": "Kombinierte Reichweite", - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high" - }, - { - "mainBarValue": 0, - "rangeUnits": "km", - "rangeValue": "96", - "secondaryBarValue": 0, - "infoIconId": 59681, - "rangeIconId": 0, - "levelIconId": 0, - "showsBar": false, - "showBarGoal": false, - "infoLabel": "Erweiterte Reichweite", - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high" - } - ], - "timestampMessage": "Aktualisiert vom Fahrzeug 3.1.2022 07:54 PM", - "chargingProfile": { - "reductionOfChargeCurrent": { - "start": { - "hour": 11, - "minute": 0 - }, - "end": { - "hour": 14, - "minute": 30 - } - }, - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingControlType": "weeklyPlanner", - "departureTimes": [ - { - "id": 1, - "action": "deactivate", - "timeStamp": { - "hour": 16, - "minute": 0 - }, - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ] - }, - { - "id": 2, - "action": "activate", - "timeStamp": { - "hour": 12, - "minute": 2 - }, - "timerWeekDays": [ - "sunday" - ] - }, - { - "id": 3, - "action": "deactivate", - "timeStamp": { - "hour": 13, - "minute": 3 - }, - "timerWeekDays": [ - "saturday" - ] - }, - { - "id": 4, - "action": "deactivate", - "timeStamp": { - "hour": 12, - "minute": 2 - }, - "timerWeekDays": [ - "sunday" - ] - } - ], - "climatisationOn": false, - "chargingSettings": { - "targetSoc": 100, - "isAcCurrentLimitActive": false, - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION" - } - } - }, - "valid": false -} - diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/f11-raw.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/f11-raw.json deleted file mode 100644 index 9fb517b38c67d..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/f11-raw.json +++ /dev/null @@ -1,283 +0,0 @@ - { - "a4aType": "NOT_SUPPORTED", - "bodyType": "F11", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Ventilation" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "climateTimer": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true, - "page": { - "description": "By setting a start time you let the vehicle know when you plan to use it.", - "primaryButtonText": "SEND TO VEHICLE", - "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE", - "subtitle": "Set start time", - "title": "Ventilation timer" - }, - "tile": { - "description": "Plan start time", - "iconId": 59774, - "title": "Ventilation timer" - } - }, - "isBmwChargingSupported": false, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": false, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": false, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": false, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": false, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "NOT_CAPABLE" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": false, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [], - "driveTrain": "COMBUSTION", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "F010-12-11-503", - "exFactoryPUStep": "1112", - "headUnit": "ID5", - "hmiVersion": "ID4", - "iStep": "F010-12-11-503", - "isLscSupported": false, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "530d", - "properties": { - "checkControlMessages": [], - "climateControl": { - - }, - "doorsAndWindows": { - "doors": { - - }, - "windows": { - - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 24 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-03-10T08:02:08Z", - "originCountryISO": "GB", - "serviceRequired": [ - { - "dateTime": "2022-10-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2022-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 25000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2024-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 60000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - } - ] - }, - "puStep": "1112", - "status": { - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - }, - { - "criticalness": "semiCritical", - "iconId": 60217, - "id": "229", - "longDescription": "Charge by driving for longer periods or use external charger. Functions requiring battery will be switched off.", - "state": "Medium", - "title": "Battery discharged: Start engine" - }, - { - "criticalness": "nonCritical", - "iconId": 60217, - "id": "50", - "longDescription": "System unable to monitor tire pressure. Check tire pressures manually. Continued driving possible. Consult service center.", - "state": "Low", - "title": "Flat Tire Monitor (FTM) inactive" - } - ], - "checkControlMessagesGeneralState": "Multiple Issues", - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59726, - "state": "Unknown", - "title": "All doors" - }, - { - "criticalness": "nonCritical", - "iconId": 59701, - "state": "Unknown", - "title": "Left front window" - }, - { - "criticalness": "nonCritical", - "iconId": 59700, - "state": "Unknown", - "title": "Right front window" - }, - { - "criticalness": "nonCritical", - "iconId": 59703, - "state": "Unknown", - "title": "Left rear window" - }, - { - "criticalness": "nonCritical", - "iconId": 59702, - "state": "Unknown", - "title": "Right rear window" - }, - { - "criticalness": "nonCritical", - "iconId": 59721, - "state": "Unknown", - "title": "Back window" - } - ], - "doorsGeneralState": "Unknown", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": true, - "levelIconId": 59682, - "levelUnits": "l", - "levelValue": "24", - "mainBarValue": 0, - "rangeIconId": 59681, - "rangeUnits": "mi", - "rangeValue": "- -", - "secondaryBarValue": 0, - "showsBar": false - } - ], - "lastUpdatedAt": "2021-03-10T08:02:08Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in October 2022", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in October 2022 or 15534 mi", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in October 2024 or 37282 mi", - "title": "Vehicle check" - } - ], - "timestampMessage": "Updated from vehicle 3/11/2021 08:02 AM" - }, - "telematicsUnit": "TCB1", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 158, - "green": 158, - "red": 158 - } - }, - "vin": "some_vin_F11", - "year": 2012 -} diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/two-vehicles.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/two-vehicles.json deleted file mode 100644 index 59cf181bbc589..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/TwoVehicles/two-vehicles.json +++ /dev/null @@ -1,665 +0,0 @@ -[ - { - "vin": "anonymous", - "model": "i3 94 (+ REX)", - "year": 2017, - "brand": "BMW", - "headUnit": "ID5", - "isLscSupported": true, - "driveTrain": "ELECTRIC", - "puStep": "0321", - "iStep": "I001-21-03-530", - "telematicsUnit": "TCB1", - "hmiVersion": "ID4", - "bodyType": "I01", - "a4aType": "USB_ONLY", - "exFactoryPUStep": "0717", - "exFactoryILevel": "I001-17-07-500", - "capabilities": { - "isRemoteServicesBookingRequired": false, - "isRemoteServicesActivationRequired": false, - "isRemoteHistorySupported": true, - "canRemoteHistoryBeDeleted": false, - "isChargingHistorySupported": true, - "isScanAndChargeSupported": true, - "isDCSContractManagementSupported": true, - "isBmwChargingSupported": true, - "isMiniChargingSupported": false, - "isChargeNowForBusinessSupported": true, - "isDataPrivacyEnabled": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingTargetSocEnable": false, - "isChargingLoudnessEnable": false, - "isChargingSettingsEnabled": false, - "isChargingHospitalityEnabled": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isCustomerEsimSupported": false, - "isCarSharingSupported": false, - "isEasyChargeSupported": false, - "lock": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug verriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "unlock": { - "isEnabled": true, - "isPinAuthenticationRequired": true, - "executionMessage": "Jetzt Ihr Fahrzeug entriegeln? Remote-Funktionen können einige Sekunden dauern." - }, - "lights": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Scheinwerfer aufleuchten lassen? Remote-Funktionen können einige Sekunden dauern." - }, - "horn": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Hupen ist in vielen Ländern nur in bestimmten Situationen erlaubt. Die Verantwortung für den Einsatz und die Einhaltung der jeweils geltenden Bestimmungen liegt allein bei Ihnen als Nutzer. \n\nJetzt hupen? Remote-Funktionen können einige Sekunden dauern." - }, - "vehicleFinder": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt Ihr Fahrzeug finden? Remote-Funktionen können einige Sekunden dauern." - }, - "sendPoi": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt POI senden? Remote-Funktionen können einige Sekunden dauern." - }, - "climateNow": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Jetzt belüften? Remote-Funktionen können einige Sekunden dauern." - } - }, - "properties": { - "lastUpdatedAt": "2022-01-03T18:54:57Z", - "inMotion": false, - "areDoorsLocked": true, - "originCountryISO": "DE", - "areDoorsClosed": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "windows": { - "driverFront": "CLOSED", - "passengerFront": "CLOSED" - }, - "trunk": "CLOSED", - "hood": "CLOSED", - "moonroof": "CLOSED" - }, - "isServiceRequired": false, - "fuelLevel": { - "value": 7, - "units": "LITERS" - }, - "chargingState": { - "chargePercentage": 47, - "state": "NOT_CHARGING", - "type": "NOT_AVAILABLE", - "isChargerConnected": false - }, - "combustionRange": { - "chargePercentage": 0, - "distance": { - "value": 96, - "units": "KILOMETERS" - } - }, - "combinedRange": { - "chargePercentage": 0, - "distance": { - "value": 96, - "units": "KILOMETERS" - } - }, - "electricRange": { - "chargePercentage": 0, - "distance": { - "value": 78, - "units": "KILOMETERS" - } - }, - "electricRangeAndStatus": { - "chargePercentage": 47, - "distance": { - "value": 78, - "units": "KILOMETERS" - } - }, - "checkControlMessages": [], - "serviceRequired": [ - { - "type": "BRAKE_FLUID", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_CHECK", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "OIL", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_TUV", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - } - ], - "vehicleLocation": { - "coordinates": { - "latitude": 1.234, - "longitude": 9.876 - }, - "address": { - "formatted": "anonymous" - }, - "heading": 39 - } - }, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "status": { - "lastUpdatedAt": "2022-01-03T18:54:57Z", - "currentMileage": { - "mileage": 32179, - "units": "km", - "formattedMileage": "32.179" - }, - "issues": null, - "doorsGeneralState": "Verriegelt", - "checkControlMessagesGeneralState": "Keine Probleme", - "doorsAndWindows": [ - { - "iconId": 59757, - "title": "Verriegelungsstatus", - "state": "Verriegelt", - "criticalness": "nonCritical" - }, - { - "iconId": 59722, - "title": "Alle Türen", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59725, - "title": "Alle Fenster", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59706, - "title": "Frontklappe", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59704, - "title": "Gepäckraum", - "state": "Geschlossen", - "criticalness": "nonCritical" - }, - { - "iconId": 59705, - "title": "Glasdach", - "state": "Geschlossen", - "criticalness": "nonCritical" - } - ], - "checkControlMessages": [], - "requiredServices": [ - { - "id": "BrakeFluid", - "title": "Bremsflüssigkeit", - "iconId": 60223, - "longDescription": "Nächster Wechsel spätestens zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleCheck", - "title": "Fahrzeug-Check", - "iconId": 60215, - "longDescription": "Nächste Sichtprüfung nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "Oil", - "title": "Motoröl", - "iconId": 60197, - "longDescription": "Nächster Wechsel nach der angegebenen Fahrstrecke oder zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleAdmissionTest", - "title": "Fahrzeuginspektion (HU)", - "iconId": 60111, - "longDescription": "Nächste gesetzliche Fahrzeuguntersuchung zum angegebenen Termin.", - "subtitle": "Fällig im November 2023", - "criticalness": "nonCritical" - } - ], - "fuelIndicators": [ - { - "mainBarValue": 47, - "rangeUnits": "km", - "rangeValue": "78", - "levelUnits": "%", - "levelValue": "47", - "secondaryBarValue": 0, - "infoIconId": 59694, - "rangeIconId": 59683, - "levelIconId": 59694, - "showsBar": true, - "showBarGoal": false, - "infoLabel": "Ladezustand", - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingStatusType": "DEFAULT", - "chargingStatusIndicatorType": "DEFAULT" - }, - { - "mainBarValue": 0, - "rangeUnits": "km", - "rangeValue": "174", - "secondaryBarValue": 0, - "infoIconId": 59691, - "rangeIconId": 59691, - "levelIconId": 0, - "showsBar": false, - "showBarGoal": false, - "infoLabel": "Kombinierte Reichweite", - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high" - }, - { - "mainBarValue": 0, - "rangeUnits": "km", - "rangeValue": "96", - "secondaryBarValue": 0, - "infoIconId": 59681, - "rangeIconId": 0, - "levelIconId": 0, - "showsBar": false, - "showBarGoal": false, - "infoLabel": "Erweiterte Reichweite", - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high" - } - ], - "timestampMessage": "Aktualisiert vom Fahrzeug 3.1.2022 07:54 PM", - "chargingProfile": { - "reductionOfChargeCurrent": { - "start": { - "hour": 11, - "minute": 0 - }, - "end": { - "hour": 14, - "minute": 30 - } - }, - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingControlType": "weeklyPlanner", - "departureTimes": [ - { - "id": 1, - "action": "deactivate", - "timeStamp": { - "hour": 16, - "minute": 0 - }, - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ] - }, - { - "id": 2, - "action": "activate", - "timeStamp": { - "hour": 12, - "minute": 2 - }, - "timerWeekDays": [ - "sunday" - ] - }, - { - "id": 3, - "action": "deactivate", - "timeStamp": { - "hour": 13, - "minute": 3 - }, - "timerWeekDays": [ - "saturday" - ] - }, - { - "id": 4, - "action": "deactivate", - "timeStamp": { - "hour": 12, - "minute": 2 - }, - "timerWeekDays": [ - "sunday" - ] - } - ], - "climatisationOn": false, - "chargingSettings": { - "targetSoc": 100, - "isAcCurrentLimitActive": false, - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION" - } - } - }, - "valid": false - } -, - { - "a4aType": "NOT_SUPPORTED", - "bodyType": "F11", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Ventilation" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "climateTimer": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "isToggleEnabled": true, - "page": { - "description": "By setting a start time you let the vehicle know when you plan to use it.", - "primaryButtonText": "SEND TO VEHICLE", - "secondaryButtonText": "DEACTIVATE AND SEND TO VEHICLE", - "subtitle": "Set start time", - "title": "Ventilation timer" - }, - "tile": { - "description": "Plan start time", - "iconId": 59774, - "title": "Ventilation timer" - } - }, - "isBmwChargingSupported": false, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": false, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": false, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": false, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": false, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "NOT_CAPABLE" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": false, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [], - "driveTrain": "COMBUSTION", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "F010-12-11-503", - "exFactoryPUStep": "1112", - "headUnit": "ID5", - "hmiVersion": "ID4", - "iStep": "F010-12-11-503", - "isLscSupported": false, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "530d", - "properties": { - "checkControlMessages": [], - "climateControl": {}, - "doorsAndWindows": { - "doors": {}, - "windows": {} - }, - "fuelLevel": { - "units": "LITERS", - "value": 24 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-03-10T08:02:08Z", - "originCountryISO": "GB", - "serviceRequired": [ - { - "dateTime": "2022-10-01T00:00:00.000Z", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2022-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 25000 - }, - "status": "OK", - "type": "OIL" - }, - { - "dateTime": "2024-10-01T00:00:00.000Z", - "distance": { - "units": "KILOMETERS", - "value": 60000 - }, - "status": "OK", - "type": "VEHICLE_CHECK" - } - ] - }, - "puStep": "1112", - "status": { - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - }, - { - "criticalness": "semiCritical", - "iconId": 60217, - "id": "229", - "longDescription": "Charge by driving for longer periods or use external charger. Functions requiring battery will be switched off.", - "state": "Medium", - "title": "Battery discharged: Start engine" - }, - { - "criticalness": "nonCritical", - "iconId": 60217, - "id": "50", - "longDescription": "System unable to monitor tire pressure. Check tire pressures manually. Continued driving possible. Consult service center.", - "state": "Low", - "title": "Flat Tire Monitor (FTM) inactive" - } - ], - "checkControlMessagesGeneralState": "Multiple Issues", - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59726, - "state": "Unknown", - "title": "All doors" - }, - { - "criticalness": "nonCritical", - "iconId": 59701, - "state": "Unknown", - "title": "Left front window" - }, - { - "criticalness": "nonCritical", - "iconId": 59700, - "state": "Unknown", - "title": "Right front window" - }, - { - "criticalness": "nonCritical", - "iconId": 59703, - "state": "Unknown", - "title": "Left rear window" - }, - { - "criticalness": "nonCritical", - "iconId": 59702, - "state": "Unknown", - "title": "Right rear window" - }, - { - "criticalness": "nonCritical", - "iconId": 59721, - "state": "Unknown", - "title": "Back window" - } - ], - "doorsGeneralState": "Unknown", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": true, - "levelIconId": 59682, - "levelUnits": "l", - "levelValue": "24", - "mainBarValue": 0, - "rangeIconId": 59681, - "rangeUnits": "mi", - "rangeValue": "- -", - "secondaryBarValue": 0, - "showsBar": false - } - ], - "lastUpdatedAt": "2021-03-10T08:02:08Z", - "recallExternalUrl": null, - "recallMessages": [], - "requiredServices": [ - { - "criticalness": "nonCritical", - "iconId": 60223, - "id": "BrakeFluid", - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in October 2022", - "title": "Brake fluid" - }, - { - "criticalness": "nonCritical", - "iconId": 60197, - "id": "Oil", - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in October 2022 or 15534 mi", - "title": "Engine oil" - }, - { - "criticalness": "nonCritical", - "iconId": 60215, - "id": "VehicleCheck", - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in October 2024 or 37282 mi", - "title": "Vehicle check" - } - ], - "timestampMessage": "Updated from vehicle 3/11/2021 08:02 AM" - }, - "telematicsUnit": "TCB1", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 158, - "green": 158, - "red": 158 - } - }, - "vin": "some_vin_F11", - "year": 2012 - } -] diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/two-weeks-timer.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/two-weeks-timer.json deleted file mode 100644 index 7b49d4fdd2699..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/two-weeks-timer.json +++ /dev/null @@ -1,301 +0,0 @@ -[ - { - "a4aType": "USB_ONLY", - "bodyType": "F45", - "brand": "BMW", - "capabilities": { - "canRemoteHistoryBeDeleted": false, - "climateNow": { - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "iconId": 59733, - "popupType": "DIALOG", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "title": "Start Climatization" - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - }, - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnable": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnable": false, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeSupported": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isMiniChargingSupported": false, - "isRemoteHistorySupported": true, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "lights": { - "executionMessage": "Flash headlights now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "lock": { - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "sendPoi": { - "executionMessage": "Send POI now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - }, - "unlock": { - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": true - }, - "vehicleFinder": { - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds.", - "isEnabled": true, - "isPinAuthenticationRequired": false - } - }, - "connectedDriveServices": [], - "driveTrain": "PLUGIN_HYBRID", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8", - "title": "BMW\nDriver's Guide" - }, - "exFactoryILevel": "F056-16-07-502", - "exFactoryPUStep": "0716", - "headUnit": "ID5", - "hmiVersion": "ID4", - "iStep": "F056-20-07-550", - "isLscSupported": true, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "model": "225xe iPerformance", - "properties": { - "areDoorsClosed": true, - "areDoorsLocked": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "chargingState": { - "chargePercentage": 40, - "isChargerConnected": false, - "state": "NOT_CHARGING", - "type": "CONDUCTIVE" - }, - "checkControlMessages": [], - "climateControl": {}, - "combinedRange": { - "distance": { - "units": "KILOMETERS", - "value": 245 - } - }, - "combustionRange": { - "distance": { - "units": "KILOMETERS", - "value": 245 - } - }, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "hood": "CLOSED", - "trunk": "CLOSED", - "windows": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - } - }, - "electricRange": { - "distance": { - "units": "KILOMETERS", - "value": 4 - } - }, - "electricRangeAndStatus": { - "chargePercentage": 40, - "distance": { - "units": "KILOMETERS", - "value": 4 - } - }, - "fuelLevel": { - "units": "LITERS", - "value": 20 - }, - "inMotion": false, - "isServiceRequired": false, - "lastUpdatedAt": "2021-11-10T18:25:38Z", - "originCountryISO": "GB", - "serviceRequired": [], - "vehicleLocation": { - "address": { - "formatted": "some_formatted_address" - }, - "coordinates": { - "latitude": 12.3456, - "longitude": 34.5678 - }, - "heading": 123 - } - }, - "puStep": "0720", - "status": { - "chargingProfile": { - "chargingControlType": "twoWeeksTimer", - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "isAcCurrentLimitActive": false, - "targetSoc": 100 - }, - "climatisationOn": false, - "departureTimes": [ - { - "action": "deactivate", - "id": 1, - "timerWeekDays": [] - }, - { - "action": "deactivate", - "id": 2, - "timerWeekDays": [] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 16, - "minute": 0 - }, - "start": { - "hour": 13, - "minute": 0 - } - } - }, - "checkControlMessages": [ - { - "criticalness": "nonCritical", - "iconId": 60197, - "state": "OK", - "title": "Engine Oil" - } - ], - "checkControlMessagesGeneralState": "No Issues", - "currentMileage": { - "formattedMileage": "66720", - "mileage": 66720, - "units": "mi" - }, - "doorsAndWindows": [ - { - "criticalness": "nonCritical", - "iconId": 59722, - "state": "Closed", - "title": "All doors and windows" - } - ], - "doorsGeneralState": "Locked", - "fuelIndicators": [ - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59691, - "infoLabel": "Combined Range", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": null, - "levelUnits": null, - "levelValue": null, - "mainBarValue": 0, - "rangeIconId": 59691, - "rangeUnits": "mi", - "rangeValue": "152", - "secondaryBarValue": 0, - "showsBar": false - }, - { - "barType": null, - "chargingStatusIndicatorType": "DEFAULT", - "chargingStatusType": "DEFAULT", - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59694, - "infoLabel": "State of Charge", - "isCircleIcon": false, - "isInaccurate": false, - "levelIconId": 59694, - "levelUnits": "%", - "levelValue": "40", - "mainBarValue": 40, - "rangeIconId": 59683, - "rangeUnits": "mi", - "rangeValue": "2", - "secondaryBarValue": 0, - "showBarGoal": false, - "showsBar": true - }, - { - "chargingType": null, - "iconOpacity": "high", - "infoIconId": 59930, - "infoLabel": "Fuel Level", - "isCircleIcon": false, - "isInaccurate": true, - "levelIconId": 59682, - "levelUnits": "l", - "levelValue": "20", - "mainBarValue": 0, - "rangeIconId": 59681, - "rangeUnits": "mi", - "rangeValue": "150", - "secondaryBarValue": 0, - "showsBar": false - } - ], - "issues": {}, - "lastUpdatedAt": "2021-11-10T18:25:38Z", - "recallExternalUrl": null, - "recallMessages": [], - "timestampMessage": "Updated from vehicle 11/11/2021 06:25 PM" - }, - "telematicsUnit": "TCB1", - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "blue": 66, - "green": 66, - "red": 66 - } - }, - "vin": "anonymous", - "year": 2016 - } - ] \ No newline at end of file diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json deleted file mode 100644 index a822abefd9ae5..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/chargingprofile/weekly-planner-t2-active.json +++ /dev/null @@ -1,427 +0,0 @@ -[ - { - "vin": "anonymous", - "model": "i3 94 (+ REX)", - "year": 2017, - "brand": "BMW", - "headUnit": "ID5", - "isLscSupported": true, - "driveTrain": "ELECTRIC", - "puStep": "0321", - "iStep": "I001-21-03-530", - "telematicsUnit": "TCB1", - "hmiVersion": "ID4", - "bodyType": "I01", - "a4aType": "USB_ONLY", - "capabilities": { - "isRemoteServicesBookingRequired": false, - "isRemoteServicesActivationRequired": false, - "lock": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Lock your vehicle now? Remote functions may take a few seconds." - }, - "unlock": { - "isEnabled": true, - "isPinAuthenticationRequired": true, - "executionMessage": "Unlock your vehicle now? Remote functions may take a few seconds." - }, - "lights": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Flash headlights now? Remote functions may take a few seconds." - }, - "horn": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Using your horn is only allowed in certain situations in many countries. Responsibility for the use and adherence to the respective regulations lies solely with you as the user. \n\nDo you want to use the horn now? Remote functions may take a few seconds." - }, - "vehicleFinder": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Find your vehicle now? Remote functions may take a few seconds." - }, - "sendPoi": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Send POI now? Remote functions may take a few seconds." - }, - "lastStateCall": { - "isNonLscFeatureEnabled": false, - "lscState": "ACTIVATED" - }, - "climateNow": { - "isEnabled": true, - "isPinAuthenticationRequired": false, - "executionMessage": "Do you want to ventilate now? Remote functions may take a few seconds.", - "executionPopup": { - "executionMessage": "Turn pre-conditioning on now? Remote functions may take a few seconds.", - "popupType": "DIALOG", - "title": "Start Climatization", - "primaryButtonText": "Start", - "secondaryButtonText": "Cancel", - "iconId": 59733 - }, - "executionStopPopup": { - "executionMessage": "Stop climate control in your vehicle now? Remote functions may take a few seconds.", - "title": "Climate control is running" - } - }, - "isRemoteHistorySupported": true, - "canRemoteHistoryBeDeleted": false, - "isChargingHistorySupported": true, - "isScanAndChargeSupported": true, - "isDCSContractManagementSupported": true, - "isBmwChargingSupported": true, - "isMiniChargingSupported": false, - "isChargeNowForBusinessSupported": true, - "isDataPrivacyEnabled": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnable": false, - "isChargingTargetSocEnable": false, - "isChargingLoudnessEnable": false, - "isChargingSettingsEnabled": false, - "isChargingHospitalityEnabled": false, - "isEvGoChargingSupported": false, - "isFindChargingEnabled": true, - "isCustomerEsimSupported": false, - "isCarSharingSupported": false, - "isEasyChargeSupported": false - }, - "connectedDriveServices": [], - "properties": { - "lastUpdatedAt": "2021-12-21T16:46:02Z", - "inMotion": false, - "areDoorsLocked": true, - "originCountryISO": "DE", - "areDoorsClosed": true, - "areDoorsOpen": false, - "areWindowsClosed": true, - "doorsAndWindows": { - "doors": { - "driverFront": "CLOSED", - "driverRear": "CLOSED", - "passengerFront": "CLOSED", - "passengerRear": "CLOSED" - }, - "windows": { - "driverFront": "CLOSED", - "passengerFront": "CLOSED" - }, - "trunk": "CLOSED", - "hood": "CLOSED", - "moonroof": "CLOSED" - }, - "isServiceRequired": false, - "fuelLevel": { - "value": 4, - "units": "LITERS" - }, - "chargingState": { - "chargePercentage": 74, - "state": "NOT_CHARGING", - "type": "NOT_AVAILABLE", - "isChargerConnected": false - }, - "combustionRange": { - "distance": { - "value": 31, - "units": "KILOMETERS" - } - }, - "combinedRange": { - "distance": { - "value": 31, - "units": "KILOMETERS" - } - }, - "electricRange": { - "distance": { - "value": 76, - "units": "KILOMETERS" - } - }, - "electricRangeAndStatus": { - "chargePercentage": 74, - "distance": { - "value": 76, - "units": "KILOMETERS" - } - }, - "checkControlMessages": [], - "serviceRequired": [ - { - "type": "BRAKE_FLUID", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_CHECK", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "OIL", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - }, - { - "type": "VEHICLE_TUV", - "status": "OK", - "dateTime": "2023-11-01T00:00:00.000Z" - } - ], - "vehicleLocation": { - "coordinates": { - "latitude": 1.2345, - "longitude": 6.789 - }, - "address": { - "formatted": "anonymous" - }, - "heading": 222 - }, - "climateControl": { - - } - }, - "isMappingPending": false, - "isMappingUnconfirmed": false, - "driverGuideInfo": { - "title": "BMW\nDriver's Guide", - "androidAppScheme": "com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" - }, - "themeSpecs": { - "vehicleStatusBackgroundColor": { - "red": 156, - "green": 154, - "blue": 152 - } - }, - "status": { - "lastUpdatedAt": "2021-12-21T16:46:02Z", - "currentMileage": { - "mileage": 31537, - "units": "km", - "formattedMileage": "31537" - }, - "issues": { - - }, - "doorsGeneralState": "Locked", - "checkControlMessagesGeneralState": "No Issues", - "doorsAndWindows": [ - { - "iconId": 59757, - "title": "Lock status", - "state": "Locked", - "criticalness": "nonCritical" - }, - { - "iconId": 59722, - "title": "All doors", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59725, - "title": "All windows", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59706, - "title": "Hood", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59704, - "title": "Trunk", - "state": "Closed", - "criticalness": "nonCritical" - }, - { - "iconId": 59705, - "title": "Sunroof", - "state": "Closed", - "criticalness": "nonCritical" - } - ], - "checkControlMessages": [], - "requiredServices": [ - { - "id": "BrakeFluid", - "title": "Brake fluid", - "iconId": 60223, - "longDescription": "Next service due by the specified date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleCheck", - "title": "Vehicle check", - "iconId": 60215, - "longDescription": "Next vehicle check due after the specified distance or date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - }, - { - "id": "Oil", - "title": "Engine oil", - "iconId": 60197, - "longDescription": "Next service due after the specified distance or date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - }, - { - "id": "VehicleAdmissionTest", - "title": "Vehicle Inspection", - "iconId": 60111, - "longDescription": "Next state inspection due by the specified date.", - "subtitle": "Due in November 2023", - "criticalness": "nonCritical" - } - ], - "recallMessages": [], - "recallExternalUrl": null, - "fuelIndicators": [ - { - "mainBarValue": 74, - "secondaryBarValue": 0, - "infoIconId": 59694, - "rangeIconId": 59683, - "rangeUnits": "km", - "rangeValue": "76", - "levelIconId": 59694, - "showsBar": true, - "levelUnits": "%", - "levelValue": "74", - "showBarGoal": false, - "barType": null, - "infoLabel": "State of Charge", - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null, - "chargingStatusType": "DEFAULT", - "chargingStatusIndicatorType": "DEFAULT" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59691, - "infoLabel": "Combined Range", - "rangeIconId": 59691, - "rangeUnits": "km", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null, - "rangeValue": "107" - }, - { - "mainBarValue": 0, - "secondaryBarValue": 0, - "infoIconId": 59681, - "infoLabel": "Extended Range", - "rangeIconId": null, - "rangeUnits": "km", - "rangeValue": "31", - "levelIconId": null, - "showsBar": false, - "levelUnits": null, - "levelValue": null, - "isInaccurate": false, - "isCircleIcon": false, - "iconOpacity": "high", - "chargingType": null - } - ], - "timestampMessage": "Updated from vehicle 12/21/2021 05:46 PM", - "chargingProfile": { - "reductionOfChargeCurrent": { - "start": { - "hour": 11, - "minute": 0 - }, - "end": { - "hour": 14, - "minute": 30 - } - }, - "chargingMode": "immediateCharging", - "chargingPreference": "chargingWindow", - "chargingControlType": "weeklyPlanner", - "departureTimes": [ - { - "id": 1, - "action": "deactivate", - "timerWeekDays": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "timeStamp": { - "hour": 16, - "minute": 0 - } - }, - { - "id": 2, - "action": "activate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - }, - { - "id": 3, - "action": "deactivate", - "timerWeekDays": [ - "saturday" - ], - "timeStamp": { - "hour": 13, - "minute": 3 - } - }, - { - "id": 4, - "action": "deactivate", - "timerWeekDays": [ - "sunday" - ], - "timeStamp": { - "hour": 12, - "minute": 2 - } - } - ], - "climatisationOn": false, - "chargingSettings": { - "targetSoc": 100, - "isAcCurrentLimitActive": false, - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION" - } - } - }, - "exFactoryPUStep": "0717", - "exFactoryILevel": "I001-17-07-500" - } -] diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/service-error.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/service-error.json index 3e645c5930b00..c338dada95ec5 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/service-error.json +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/remote_services/service-error.json @@ -2,7 +2,7 @@ "eventStatus": "ERROR", "errorDetails": { "title": "Etwas ist schiefgelaufen", - "description": "Die folgenden Einschränkungen verbieten die Ausführung von Remote Services: Aus Sicherheitsgründen sind Remote Services nicht verfügbar, wenn die Fahrbereitschaft eingeschaltet ist. Remote Services können nur mit einem ausreichenden Ladezustand durchgeführt werden. Die Remote Services „Verriegeln“ und „Entriegeln“ können nur ausgeführt werden, wenn die Fahrertür geschlossen und der Türstatus bekannt ist.", + "description": "Die folgenden Einschränkungen verbieten die Ausführung von Remote Services: Aus Sicherheitsgründen sind Remote Services nicht verfügbar, wenn die Fahrbereitschaft eingeschaltet ist. Remote Services können nur mit einem ausreichenden Ladezustand durchgeführt werden. Die Remote Services „Verriegeln" und „Entriegeln" können nur ausgeführt werden, wenn die Fahrertür geschlossen und der Türstatus bekannt ist.", "presentationType": "PAGE", "iconId": 60217, "isRetriable": true, diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/vehicles.json b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/vehicles.json new file mode 100644 index 0000000000000..b8fcb3a06c035 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/vehicles.json @@ -0,0 +1,603 @@ +[ + { + "vehicleBase": { + "vin": "VIN1234567", + "attributes": { + "lastFetched": "2023-01-02T19:52:55.678Z", + "model": "M340i xDrive", + "year": 2022, + "color": 4284572001, + "brand": "BMW", + "driveTrain": "MILD_HYBRID", + "headUnitType": "MGU", + "headUnitRaw": "HU_MGU", + "hmiVersion": "ID7", + "telematicsUnit": "ATM2", + "bodyType": "G21", + "countryOfOrigin": "DE" + } + }, + "vehicleState": { + "state": { + "isLeftSteering": true, + "lastFetched": "2023-01-02T19:52:56.420Z", + "lastUpdatedAt": "2023-01-01T22:53:03Z", + "isLscSupported": true, + "range": 224, + "doorsState": { + "combinedSecurityState": "SECURED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "rear": "CLOSED", + "combinedState": "CLOSED" + }, + "roofState": { + "roofState": "CLOSED", + "roofStateType": "SUN_ROOF" + }, + "tireState": { + "frontLeft": { + "details": { + "dimension": "225/45 R18 95V XL", + "treadDesign": "Winter Contact TS 860 S SSR", + "manufacturer": "Continental", + "manufacturingWeek": 5299, + "isOptimizedForOemBmw": true, + "partNumber": "2471558", + "speedClassification": { + "speedRating": 240, + "atLeast": false + }, + "mountingDate": "2022-10-06T00:00:00.000Z", + "season": 4, + "identificationInProgress": false + }, + "status": { + "currentPressure": 250, + "targetPressure": 270 + } + }, + "frontRight": { + "details": { + "dimension": "225/45 R18 95V XL", + "treadDesign": "Winter Contact TS 860 S SSR", + "manufacturer": "Continental", + "manufacturingWeek": 5299, + "isOptimizedForOemBmw": true, + "partNumber": "2471558", + "speedClassification": { + "speedRating": 240, + "atLeast": false + }, + "mountingDate": "2022-10-06T00:00:00.000Z", + "season": 4, + "identificationInProgress": false + }, + "status": { + "currentPressure": 250, + "targetPressure": 270 + } + }, + "rearLeft": { + "details": { + "dimension": "255/40 R18 99V XL", + "treadDesign": "Winter Contact TS 860 S SSR", + "manufacturer": "Continental", + "manufacturingWeek": 5299, + "isOptimizedForOemBmw": true, + "partNumber": "2471559", + "speedClassification": { + "speedRating": 240, + "atLeast": false + }, + "mountingDate": "2022-10-06T00:00:00.000Z", + "season": 4, + "identificationInProgress": false + }, + "status": { + "currentPressure": 250, + "targetPressure": 270 + } + }, + "rearRight": { + "details": { + "dimension": "255/40 R18 99V XL", + "treadDesign": "Winter Contact TS 860 S SSR", + "manufacturer": "Continental", + "manufacturingWeek": 5299, + "isOptimizedForOemBmw": true, + "partNumber": "2471559", + "speedClassification": { + "speedRating": 240, + "atLeast": false + }, + "mountingDate": "2022-10-06T00:00:00.000Z", + "season": 4, + "identificationInProgress": false + }, + "status": { + "currentPressure": 260, + "targetPressure": 270 + } + } + }, + "location": { + "coordinates": { + "latitude": 2.34567, + "longitude": 3.45678 + }, + "address": { + "formatted": "Teststraße 123, 11111 Testort" + }, + "heading": 184 + }, + "currentMileage": 4573, + "climateControlState": { + "activity": "INACTIVE" + }, + "requiredServices": [ + { + "dateTime": "2024-06-01T00:00:00.000Z", + "mileage": 29000, + "type": "OIL", + "status": "OK", + "description": "Next service due after the specified distance or date." + }, + { + "dateTime": "2025-06-01T00:00:00.000Z", + "mileage": -1, + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Next service due by the specified date." + }, + { + "dateTime": "2025-07-01T00:00:00.000Z", + "mileage": -1, + "type": "VEHICLE_TUV", + "status": "OK", + "description": "Next state inspection due by the specified date." + }, + { + "dateTime": "2026-06-01T00:00:00.000Z", + "mileage": 60000, + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Next visual inspection due by specified date or, if shown, when stated distance has been reached." + } + ], + "checkControlMessages": [ + { + "type": "TIRE_PRESSURE", + "severity": "LOW", + "id": -1, + "description": "", + "name": "" + }, + { + "type": "ENGINE_OIL", + "severity": "LOW", + "id": -1, + "description": "", + "name": "" + } + ], + "combustionFuelLevel": { + "remainingFuelPercent": 33, + "remainingFuelLiters": 17, + "range": 224 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "electricChargingState": { + "chargingConnectionType": "", + "chargingStatus": "", + "isChargerConnected": false, + "chargingTarget": -1, + "chargingLevelPercent": -1, + "range": -1 + }, + "isDeepSleepModeActive": false, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ], + "chargingProfile": { + "INVALID_TIMER": { + "id": -1 + }, + "climatisationOn": false + } + }, + "capabilities": { + "checkSustainabilityDPP": false, + "climateNow": true, + "horn": true, + "isBmwChargingSupported": false, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": false, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": false, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isClimateTimerSupported": true, + "isClimateTimerWeeklyActive": true, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": false, + "isEasyChargeEnabled": false, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": true, + "lights": true, + "lock": true, + "remote360": false, + "remoteChargingCommands": { + "chargingControl": [], + "flapControl": [], + "plugControl": [] + }, + "remoteSoftwareUpgrade": true, + "sendPoi": true, + "speechThirdPartyAlexa": true, + "speechThirdPartyAlexaSDK": false, + "unlock": true, + "vehicleFinder": true, + "digitalKey": { + "bookedServicePackage": "NONE", + "readerGraphics": "", + "state": "NOT_AVAILABLE" + }, + "a4aType": "NOT_SUPPORTED", + "climateFunction": "VENTILATION", + "climateTimerTrigger": "DEPARTURE_TIMER", + "lastStateCallState": "ACTIVATED", + "vehicleStateSource": "LAST_STATE_CALL" + }, + "rawStateJson": "{\"state\":{\"isLeftSteering\":true,\"lastFetched\":\"2023-01-02T19:52:56.420Z\",\"lastUpdatedAt\":\"2023-01-01T22:53:03Z\",\"isLscSupported\":true,\"range\":224,\"doorsState\":{\"combinedSecurityState\":\"SECURED\",\"leftFront\":\"CLOSED\",\"leftRear\":\"CLOSED\",\"rightFront\":\"CLOSED\",\"rightRear\":\"CLOSED\",\"combinedState\":\"CLOSED\",\"hood\":\"CLOSED\",\"trunk\":\"CLOSED\"},\"windowsState\":{\"leftFront\":\"CLOSED\",\"leftRear\":\"CLOSED\",\"rightFront\":\"CLOSED\",\"rightRear\":\"CLOSED\",\"rear\":\"CLOSED\",\"combinedState\":\"CLOSED\"},\"roofState\":{\"roofState\":\"CLOSED\",\"roofStateType\":\"SUN_ROOF\"},\"tireState\":{\"frontLeft\":{\"details\":{\"dimension\":\"225/45 R18 95V XL\",\"treadDesign\":\"Winter Contact TS 860 S SSR\",\"manufacturer\":\"Continental\",\"manufacturingWeek\":5299,\"isOptimizedForOemBmw\":true,\"partNumber\":\"2471558\",\"speedClassification\":{\"speedRating\":240,\"atLeast\":false},\"mountingDate\":\"2022-10-06T00:00:00.000Z\",\"season\":4,\"identificationInProgress\":false},\"status\":{\"currentPressure\":250,\"targetPressure\":270}},\"frontRight\":{\"details\":{\"dimension\":\"225/45 R18 95V XL\",\"treadDesign\":\"Winter Contact TS 860 S SSR\",\"manufacturer\":\"Continental\",\"manufacturingWeek\":5299,\"isOptimizedForOemBmw\":true,\"partNumber\":\"2471558\",\"speedClassification\":{\"speedRating\":240,\"atLeast\":false},\"mountingDate\":\"2022-10-06T00:00:00.000Z\",\"season\":4,\"identificationInProgress\":false},\"status\":{\"currentPressure\":250,\"targetPressure\":270}},\"rearLeft\":{\"details\":{\"dimension\":\"255/40 R18 99V XL\",\"treadDesign\":\"Winter Contact TS 860 S SSR\",\"manufacturer\":\"Continental\",\"manufacturingWeek\":5299,\"isOptimizedForOemBmw\":true,\"partNumber\":\"2471559\",\"speedClassification\":{\"speedRating\":240,\"atLeast\":false},\"mountingDate\":\"2022-10-06T00:00:00.000Z\",\"season\":4,\"identificationInProgress\":false},\"status\":{\"currentPressure\":250,\"targetPressure\":270}},\"rearRight\":{\"details\":{\"dimension\":\"255/40 R18 99V XL\",\"treadDesign\":\"Winter Contact TS 860 S SSR\",\"manufacturer\":\"Continental\",\"manufacturingWeek\":5299,\"isOptimizedForOemBmw\":true,\"partNumber\":\"2471559\",\"speedClassification\":{\"speedRating\":240,\"atLeast\":false},\"mountingDate\":\"2022-10-06T00:00:00.000Z\",\"season\":4,\"identificationInProgress\":false},\"status\":{\"currentPressure\":260,\"targetPressure\":270}}},\"location\":{\"coordinates\":{\"latitude\":2.34567,\"longitude\":3.45678},\"address\":{\"formatted\":\"Teststraße 123, 11111 Testort\"},\"heading\":184},\"currentMileage\":4573,\"climateControlState\":{\"activity\":\"INACTIVE\"},\"requiredServices\":[{\"dateTime\":\"2024-06-01T00:00:00.000Z\",\"mileage\":29000,\"type\":\"OIL\",\"status\":\"OK\",\"description\":\"Next service due after the specified distance or date.\"},{\"dateTime\":\"2025-06-01T00:00:00.000Z\",\"type\":\"BRAKE_FLUID\",\"status\":\"OK\",\"description\":\"Next service due by the specified date.\"},{\"dateTime\":\"2025-07-01T00:00:00.000Z\",\"type\":\"VEHICLE_TUV\",\"status\":\"OK\",\"description\":\"Next state inspection due by the specified date.\"},{\"dateTime\":\"2026-06-01T00:00:00.000Z\",\"mileage\":60000,\"type\":\"VEHICLE_CHECK\",\"status\":\"OK\",\"description\":\"Next visual inspection due by specified date or, if shown, when stated distance has been reached.\"}],\"checkControlMessages\":[{\"type\":\"TIRE_PRESSURE\",\"severity\":\"LOW\"},{\"type\":\"ENGINE_OIL\",\"severity\":\"LOW\"}],\"combustionFuelLevel\":{\"remainingFuelPercent\":33,\"remainingFuelLiters\":17,\"range\":224},\"driverPreferences\":{\"lscPrivacyMode\":\"OFF\"},\"isDeepSleepModeActive\":false,\"climateTimers\":[{\"isWeeklyTimer\":false,\"timerAction\":\"DEACTIVATE\",\"timerWeekDays\":[],\"departureTime\":{\"hour\":7,\"minute\":0}},{\"isWeeklyTimer\":true,\"timerAction\":\"DEACTIVATE\",\"timerWeekDays\":[\"MONDAY\"],\"departureTime\":{\"hour\":7,\"minute\":0}},{\"isWeeklyTimer\":true,\"timerAction\":\"DEACTIVATE\",\"timerWeekDays\":[\"MONDAY\"],\"departureTime\":{\"hour\":7,\"minute\":0}}]},\"capabilities\":{\"a4aType\":\"NOT_SUPPORTED\",\"climateNow\":true,\"isClimateTimerSupported\":true,\"climateTimerTrigger\":\"DEPARTURE_TIMER\",\"climateFunction\":\"VENTILATION\",\"horn\":true,\"isBmwChargingSupported\":false,\"isCarSharingSupported\":false,\"isChargeNowForBusinessSupported\":false,\"isChargingHistorySupported\":false,\"isChargingHospitalityEnabled\":false,\"isChargingLoudnessEnabled\":false,\"isChargingPlanSupported\":false,\"isChargingPowerLimitEnabled\":false,\"isChargingSettingsEnabled\":false,\"isChargingTargetSocEnabled\":false,\"isCustomerEsimSupported\":false,\"isDataPrivacyEnabled\":false,\"isDCSContractManagementSupported\":false,\"isEasyChargeEnabled\":false,\"isMiniChargingSupported\":false,\"isEvGoChargingSupported\":false,\"isRemoteHistoryDeletionSupported\":false,\"isRemoteEngineStartSupported\":false,\"isRemoteServicesActivationRequired\":false,\"isRemoteServicesBookingRequired\":false,\"isScanAndChargeSupported\":false,\"lastStateCallState\":\"ACTIVATED\",\"lights\":true,\"lock\":true,\"remoteSoftwareUpgrade\":true,\"sendPoi\":true,\"speechThirdPartyAlexa\":true,\"speechThirdPartyAlexaSDK\":false,\"unlock\":true,\"vehicleFinder\":true,\"vehicleStateSource\":\"LAST_STATE_CALL\",\"isRemoteHistorySupported\":true,\"isWifiHotspotServiceSupported\":true,\"isNonLscFeatureEnabled\":false,\"isSustainabilitySupported\":false,\"isSustainabilityAccumulatedViewEnabled\":false,\"checkSustainabilityDPP\":false,\"specialThemeSupport\":[],\"isRemoteParkingSupported\":false,\"remoteChargingCommands\":{},\"isClimateTimerWeeklyActive\":true,\"digitalKey\":{\"bookedServicePackage\":\"NONE\",\"state\":\"NOT_AVAILABLE\"}}}" + }, + "valid": false + }, + { + "vehicleBase": { + "vin": "VIN1234568", + "attributes": { + "lastFetched": "2023-01-02T19:52:56.255Z", + "model": "Cooper", + "year": 2022, + "color": 4290295992, + "brand": "MINI", + "driveTrain": "COMBUSTION", + "headUnitType": "ENTRY_EVO", + "headUnitRaw": "ENAVEVO", + "hmiVersion": "ID5", + "telematicsUnit": "ATM1", + "bodyType": "F56", + "countryOfOrigin": "DE" + } + }, + "vehicleState": { + "state": { + "isLeftSteering": true, + "lastFetched": "2023-01-02T19:52:57.116Z", + "lastUpdatedAt": "2023-01-02T19:03:43Z", + "isLscSupported": true, + "range": 194, + "doorsState": { + "combinedSecurityState": "SECURED", + "leftFront": "CLOSED", + "leftRear": "", + "rightFront": "CLOSED", + "rightRear": "", + "combinedState": "CLOSED", + "hood": "CLOSED", + "trunk": "CLOSED" + }, + "windowsState": { + "leftFront": "CLOSED", + "leftRear": "", + "rightFront": "CLOSED", + "rightRear": "", + "rear": "", + "combinedState": "CLOSED" + }, + "roofState": { + "roofState": "", + "roofStateType": "" + }, + "tireState": { + "frontLeft": { + "details": { + "dimension": "", + "treadDesign": "", + "manufacturer": "", + "manufacturingWeek": -1, + "isOptimizedForOemBmw": false, + "partNumber": "", + "mountingDate": "", + "season": -1, + "identificationInProgress": false + }, + "status": { + "currentPressure": -1, + "targetPressure": -1 + } + }, + "frontRight": { + "details": { + "dimension": "", + "treadDesign": "", + "manufacturer": "", + "manufacturingWeek": -1, + "isOptimizedForOemBmw": false, + "partNumber": "", + "mountingDate": "", + "season": -1, + "identificationInProgress": false + }, + "status": { + "currentPressure": -1, + "targetPressure": -1 + } + }, + "rearLeft": { + "details": { + "dimension": "", + "treadDesign": "", + "manufacturer": "", + "manufacturingWeek": -1, + "isOptimizedForOemBmw": false, + "partNumber": "", + "mountingDate": "", + "season": -1, + "identificationInProgress": false + }, + "status": { + "currentPressure": -1, + "targetPressure": -1 + } + }, + "rearRight": { + "details": { + "dimension": "", + "treadDesign": "", + "manufacturer": "", + "manufacturingWeek": -1, + "isOptimizedForOemBmw": false, + "partNumber": "", + "mountingDate": "", + "season": -1, + "identificationInProgress": false + }, + "status": { + "currentPressure": -1, + "targetPressure": -1 + } + } + }, + "location": { + "coordinates": { + "latitude": 2.34567, + "longitude": 3.45678 + }, + "address": { + "formatted": "Teststraße 123, 11111 Testort" + }, + "heading": 181 + }, + "currentMileage": 897, + "climateControlState": { + "activity": "" + }, + "requiredServices": [ + { + "dateTime": "2025-10-01T00:00:00.000Z", + "mileage": -1, + "type": "VEHICLE_TUV", + "status": "OK", + "description": "Next state inspection due by the specified date." + }, + { + "dateTime": "2024-09-01T00:00:00.000Z", + "mileage": 30000, + "type": "OIL", + "status": "OK", + "description": "Next service due after the specified distance or date." + }, + { + "dateTime": "2026-09-01T00:00:00.000Z", + "mileage": 60000, + "type": "VEHICLE_CHECK", + "status": "OK", + "description": "Next vehicle check due on the specified date or, if shown, after the specified distance." + }, + { + "dateTime": "2025-09-01T00:00:00.000Z", + "mileage": -1, + "type": "BRAKE_FLUID", + "status": "OK", + "description": "Next service due by the specified date." + } + ], + "checkControlMessages": [ + { + "type": "ENGINE_OIL", + "severity": "LOW", + "id": -1, + "description": "", + "name": "" + } + ], + "combustionFuelLevel": { + "remainingFuelPercent": -1, + "remainingFuelLiters": 13, + "range": 194 + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "electricChargingState": { + "chargingConnectionType": "", + "chargingStatus": "", + "isChargerConnected": false, + "chargingTarget": -1, + "chargingLevelPercent": -1, + "range": -1 + }, + "isDeepSleepModeActive": false, + "climateTimers": [ + { + "isWeeklyTimer": false, + "timerAction": "DEACTIVATE", + "timerWeekDays": [], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + }, + { + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": [ + "MONDAY" + ], + "departureTime": { + "hour": 7, + "minute": 0 + } + } + ], + "chargingProfile": { + "INVALID_TIMER": { + "id": -1 + }, + "climatisationOn": false + } + }, + "capabilities": { + "checkSustainabilityDPP": false, + "climateNow": true, + "horn": true, + "isBmwChargingSupported": false, + "isCarSharingSupported": true, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": false, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": false, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isClimateTimerSupported": true, + "isClimateTimerWeeklyActive": false, + "isCustomerEsimSupported": false, + "isDataPrivacyEnabled": false, + "isDCSContractManagementSupported": false, + "isEasyChargeEnabled": false, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "isSustainabilityAccumulatedViewEnabled": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lights": true, + "lock": true, + "remote360": false, + "remoteChargingCommands": { + "chargingControl": [], + "flapControl": [], + "plugControl": [] + }, + "remoteSoftwareUpgrade": false, + "sendPoi": true, + "speechThirdPartyAlexa": true, + "speechThirdPartyAlexaSDK": false, + "unlock": true, + "vehicleFinder": true, + "digitalKey": { + "bookedServicePackage": "NONE", + "readerGraphics": "", + "state": "NOT_AVAILABLE" + }, + "a4aType": "BLUETOOTH", + "climateFunction": "VENTILATION", + "climateTimerTrigger": "START_TIMER", + "lastStateCallState": "ACTIVATED", + "vehicleStateSource": "LAST_STATE_CALL" + }, + "rawStateJson": "{\"state\":{\"isLeftSteering\":true,\"lastFetched\":\"2023-01-02T19:52:57.116Z\",\"lastUpdatedAt\":\"2023-01-02T19:03:43Z\",\"isLscSupported\":true,\"range\":194,\"doorsState\":{\"combinedSecurityState\":\"SECURED\",\"leftFront\":\"CLOSED\",\"rightFront\":\"CLOSED\",\"combinedState\":\"CLOSED\",\"hood\":\"CLOSED\",\"trunk\":\"CLOSED\"},\"windowsState\":{\"leftFront\":\"CLOSED\",\"rightFront\":\"CLOSED\",\"combinedState\":\"CLOSED\"},\"location\":{\"coordinates\":{\"latitude\":2.34567,\"longitude\":3.45678},\"address\":{\"formatted\":\"Teststraße 123, 11111 Testort\"},\"heading\":181},\"currentMileage\":897,\"requiredServices\":[{\"dateTime\":\"2025-10-01T00:00:00.000Z\",\"type\":\"VEHICLE_TUV\",\"status\":\"OK\",\"description\":\"Next state inspection due by the specified date.\"},{\"dateTime\":\"2024-09-01T00:00:00.000Z\",\"mileage\":30000,\"type\":\"OIL\",\"status\":\"OK\",\"description\":\"Next service due after the specified distance or date.\"},{\"dateTime\":\"2026-09-01T00:00:00.000Z\",\"mileage\":60000,\"type\":\"VEHICLE_CHECK\",\"status\":\"OK\",\"description\":\"Next vehicle check due on the specified date or, if shown, after the specified distance.\"},{\"dateTime\":\"2025-09-01T00:00:00.000Z\",\"type\":\"BRAKE_FLUID\",\"status\":\"OK\",\"description\":\"Next service due by the specified date.\"}],\"checkControlMessages\":[{\"type\":\"ENGINE_OIL\",\"severity\":\"LOW\"}],\"combustionFuelLevel\":{\"remainingFuelLiters\":13,\"range\":194},\"driverPreferences\":{\"lscPrivacyMode\":\"OFF\"},\"climateTimers\":[{\"isWeeklyTimer\":false,\"timerAction\":\"DEACTIVATE\",\"timerWeekDays\":[],\"departureTime\":{\"hour\":7,\"minute\":0}},{\"isWeeklyTimer\":true,\"timerAction\":\"DEACTIVATE\",\"timerWeekDays\":[\"MONDAY\"],\"departureTime\":{\"hour\":7,\"minute\":0}},{\"isWeeklyTimer\":true,\"timerAction\":\"DEACTIVATE\",\"timerWeekDays\":[\"MONDAY\"],\"departureTime\":{\"hour\":7,\"minute\":0}}]},\"capabilities\":{\"a4aType\":\"BLUETOOTH\",\"climateNow\":true,\"isClimateTimerSupported\":true,\"climateTimerTrigger\":\"START_TIMER\",\"climateFunction\":\"VENTILATION\",\"horn\":true,\"isBmwChargingSupported\":false,\"isCarSharingSupported\":true,\"isChargeNowForBusinessSupported\":false,\"isChargingHistorySupported\":false,\"isChargingHospitalityEnabled\":false,\"isChargingLoudnessEnabled\":false,\"isChargingPlanSupported\":false,\"isChargingPowerLimitEnabled\":false,\"isChargingSettingsEnabled\":false,\"isChargingTargetSocEnabled\":false,\"isCustomerEsimSupported\":false,\"isDataPrivacyEnabled\":false,\"isDCSContractManagementSupported\":false,\"isEasyChargeEnabled\":false,\"isMiniChargingSupported\":false,\"isEvGoChargingSupported\":false,\"isRemoteHistoryDeletionSupported\":false,\"isRemoteEngineStartSupported\":false,\"isRemoteServicesActivationRequired\":false,\"isRemoteServicesBookingRequired\":false,\"isScanAndChargeSupported\":false,\"lastStateCallState\":\"ACTIVATED\",\"lights\":true,\"lock\":true,\"sendPoi\":true,\"speechThirdPartyAlexa\":true,\"speechThirdPartyAlexaSDK\":false,\"unlock\":true,\"vehicleFinder\":true,\"vehicleStateSource\":\"LAST_STATE_CALL\",\"isRemoteHistorySupported\":true,\"isWifiHotspotServiceSupported\":false,\"isNonLscFeatureEnabled\":false,\"isSustainabilitySupported\":false,\"isSustainabilityAccumulatedViewEnabled\":false,\"checkSustainabilityDPP\":false,\"specialThemeSupport\":[],\"isRemoteParkingSupported\":false,\"remoteChargingCommands\":{},\"isClimateTimerWeeklyActive\":false,\"digitalKey\":{\"bookedServicePackage\":\"NONE\",\"state\":\"NOT_AVAILABLE\"}}}" + }, + "valid": false + } +] \ No newline at end of file From 544272b701d362d18dbf53741f0b8154fc657a89 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Sat, 11 Feb 2023 22:46:08 +0100 Subject: [PATCH 02/64] [mybmw] fix case sensitive brand in request header Co-authored-by: Mark Herwege mark.herwege@telenet.be Also-by: Mark Herwege mark.herwege@telenet.be Signed-off-by: Martin Grassl martin.grassl@digital-filestore.de Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/handler/backend/MyBMWFileProxy.java | 3 ++- .../binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java index 1b44f62bf298e..49eb5a64e5896 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java @@ -183,7 +183,8 @@ public ExecutionStatusContainer executeRemoteServiceStatusCall(String brand, Str private String fileToString(String filename) { logger.trace("reading file {}", RESPONSES + vehicleToBeTested + filename); try (BufferedReader br = new BufferedReader(new InputStreamReader( - MyBMWFileProxy.class.getClassLoader().getResourceAsStream(RESPONSES + vehicleToBeTested + filename), "UTF-8"))) { + MyBMWFileProxy.class.getClassLoader().getResourceAsStream(RESPONSES + vehicleToBeTested + filename), + "UTF-8"))) { StringBuilder buf = new StringBuilder(); String sCurrentLine; diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java index 763f392559d02..ff6dc38a584ff 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -300,7 +300,7 @@ private synchronized byte[] call(final String url, final boolean post, final Str } req.header(HttpHeader.AUTHORIZATION, myBMWTokenHandler.getToken().getBearerToken()); - req.header(HTTPConstants.HEADER_X_USER_AGENT, String.format(BimmerConstants.X_USER_AGENT, brand, + req.header(HTTPConstants.HEADER_X_USER_AGENT, String.format(BimmerConstants.X_USER_AGENT, brand.toLowerCase(), APP_VERSIONS.get(bridgeConfiguration.region), bridgeConfiguration.region)); req.header(HttpHeader.ACCEPT_LANGUAGE, bridgeConfiguration.language); req.header(HttpHeader.ACCEPT, contentType); From 71255554239078a11bb21df9f68854ad1bb0c3dc Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 15 Feb 2023 21:19:14 +0100 Subject: [PATCH 03/64] [mybmw] added Finnish translations Co-authored-by: Jari Likonen Also-by: Jari Likonen Signed-off-by: Martin Grassl --- .../resources/OH-INF/i18n/mybmw_fi.properties | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_fi.properties diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_fi.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_fi.properties new file mode 100644 index 0000000000000..f9f309d7214f3 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_fi.properties @@ -0,0 +1,257 @@ +# Binding +binding.mybmw.name = MyBMW +binding.mybmw.description = Provides access to your Vehicle Data like MyBMW App + +# thing types +thing-type.config.mybmw.bridge.language.description = Channel data can be returned in the desired language like en, de, fr ... +thing-type.config.mybmw.bridge.language.label = Kieliasetukset +thing-type.config.mybmw.bridge.password.description = MyBMW Salasana +thing-type.config.mybmw.bridge.password.label = Salasana +thing-type.config.mybmw.bridge.region.description = Valitse alue muodostaaksesi yhteyden sopivaan BMW-palvelimeen +thing-type.config.mybmw.bridge.region.label = Alue +thing-type.config.mybmw.bridge.region.option.CHINA = Kiina +thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = Pohjois-Amerikka +thing-type.config.mybmw.bridge.region.option.ROW = Muu maailma +thing-type.config.mybmw.bridge.userName.description = MyBMW Käyttäjätunnus +thing-type.config.mybmw.bridge.userName.label = Käyttäjätunnus + +thing-type.config.mybmw.vehicle.refreshInterval.description = Ajoneuvotietojen päivitystaajuus +thing-type.config.mybmw.vehicle.refreshInterval.label = Päivitysväli +thing-type.config.mybmw.vehicle.vehicleBrand.description = Automerkki kuten BMW tai Mini +thing-type.config.mybmw.vehicle.vehicleBrand.label = Ajoneuvon merkki +thing-type.config.mybmw.vehicle.vin.description = BMW:n antama VIN +thing-type.config.mybmw.vehicle.vin.label = Ajoneuvon valmistenumero (VIN) +thing-type.mybmw.account.description = BMW-tilisi tiedot +thing-type.mybmw.account.label = MyBMW tili +thing-type.mybmw.bev_rex.description = Akkukäyttöinen sähköajoneuvo kantaman pidentäjällä (BEV_REX) +thing-type.mybmw.bev_rex.label = Sähköauto REX:llä +thing-type.mybmw.bev.description = Akkukäyttöinen sähköajoneuvo (BEV) +thing-type.mybmw.bev.label = Sähköajoneuvo +thing-type.mybmw.conv.description = Perinteisellä polttoaineella varustettu ajoneuvo (CONV) +thing-type.mybmw.conv.label = Perinteinen ajoneuvo +thing-type.mybmw.phev.description = Ladattava hybridi (PHEV) +thing-type.mybmw.phev.label = Ladattava hybridi sähköajoneuvo + +# channel group types +channel-group-type.mybmw.charge-statistic.description = Kuluvan kuukauden lataustilastot +channel-group-type.mybmw.charge-statistic.label = Lataustilastot +channel-group-type.mybmw.check-control-values.description = Näyttää nykyiset aktiiviset CheckControl-viestit +channel-group-type.mybmw.check-control-values.label = CheckControl-viestit +channel-group-type.mybmw.conv-range-values.description = Näyttää ajokilometrimäärän, jäljellä olevan toimintamatkan ja polttoainetason arvot +channel-group-type.mybmw.conv-range-values.label = Toimintamatka ja polttoainetiedot +channel-group-type.mybmw.door-values.description = Kaikkien ovien ja ikkunoiden yksityiskohtainen tila +channel-group-type.mybmw.door-values.label = Yksityiskohtainen oven tila +channel-group-type.mybmw.ev-range-values.description = Näyttää mittarilukeman, jäljellä oleva toimintamatkan ja lataustason arvot +channel-group-type.mybmw.ev-range-values.label = Kantama- ja lataustiedot +channel-group-type.mybmw.ev-vehicle-status.description = Ajoneuvon yleistila +channel-group-type.mybmw.ev-vehicle-status.label = Ajoneuvon tila +channel-group-type.mybmw.hybrid-range-values.description = Näyttää ajokilometrit, jäljellä olevan polttoaineen ja ajomatkan hybridiautoille +channel-group-type.mybmw.hybrid-range-values.label = Toimintamatka, lataus/polttoainetiedot +channel-group-type.mybmw.image-values.description = Näyttää ajoneuvosi kuvan +channel-group-type.mybmw.image-values.label = Ajoneuvon kuva + +channel-group-type.mybmw.location-values.description = Ajoneuvon koordinaatit ja suunta +channel-group-type.mybmw.location-values.label = Ajoneuvon sijainti +channel-group-type.mybmw.profile-values.description = Ajoitetut latausprofiilit +channel-group-type.mybmw.profile-values.label = Sähkölatausprofiili +channel-group-type.mybmw.remote-services.description = Ajoneuvon etäohjaus +channel-group-type.mybmw.remote-services.label = Etäpalvelut +channel-group-type.mybmw.service-values.description = Auton tulevat huoltoaikataulut +channel-group-type.mybmw.service-values.label = Ajoneuvopalvelut +channel-group-type.mybmw.session-values.description = Aiemmat lataustapahtumat +channel-group-type.mybmw.session-values.label = Lataustapahtumat +channel-group-type.mybmw.tire-pressures.description = Nykyiset ja halutut paineet kaikkiin renkaisiin +channel-group-type.mybmw.tire-pressures.label = Rengaspaine +channel-group-type.mybmw.vehicle-status.description = Ajoneuvon yleistila +channel-group-type.mybmw.vehicle-status.label = Ajoneuvon tila + +# channel types +channel-type.mybmw.address-channel.label = Osoite +channel-type.mybmw.charging-info-channel.label = Lataustiedot +channel-type.mybmw.charging-remaining-channel.label = Jäljellä oleva latausaika +channel-type.mybmw.charging-status-channel.label = Lataustila +channel-type.mybmw.check-control-channel.label = Check Control +channel-type.mybmw.checkcontrol-details-channel.label = Check Control tiedot +channel-type.mybmw.checkcontrol-name-channel.label = Check Control kuvaus +channel-type.mybmw.checkcontrol-severity-channel.label = Vakavuustaso + +channel-type.mybmw.doors-channel.label = Oven yleistila +channel-type.mybmw.driver-front-channel.label = Kuljettajan ovi +channel-type.mybmw.driver-rear-channel.label = Kuljettajan puoleinen takaovi +channel-type.mybmw.estimated-fuel-l-100km-channel.label = Arvioitu kulutus l/100km +channel-type.mybmw.estimated-fuel-mpg-channel.label = Arvioitu kulutus mpg +channel-type.mybmw.front-left-current-channel.label = Rengaspaine vasen etu +channel-type.mybmw.front-left-target-channel.label = Rengaspaine vasen etu tavoite +channel-type.mybmw.front-right-current-channel.label = Rengaspaine oikea etu +channel-type.mybmw.front-right-target-channel.label = Rengaspaine oikea etu tavoite +channel-type.mybmw.gps-channel.label = GPS koordinaatit +channel-type.mybmw.heading-channel.label = Suuntakulma +channel-type.mybmw.home-distance-channel.description = Laskettu etäisyys ajoneuvon ja kodin välillä +channel-type.mybmw.home-distance-channel.label = Etäisyys kotoa +channel-type.mybmw.hood-channel.label = Konepelti + +channel-type.mybmw.image-view-channel.command.option.FrontLeft = Vasen sivunäkymä +channel-type.mybmw.image-view-channel.command.option.FrontRight = Oikea sivunäkymä +channel-type.mybmw.image-view-channel.command.option.FrontView = Etunäkymä +channel-type.mybmw.image-view-channel.command.option.RearView = Takanäkymä +channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Näkymä edestä +channel-type.mybmw.image-view-channel.label = Kuvan katseluportti +channel-type.mybmw.last-fetched-channel.label = Viimeisin Openhab-päivityksen aikaleima +channel-type.mybmw.last-update-channel.label = Viimeisin auton tilan aikaleima +channel-type.mybmw.lock-channel.label = Ovet lukittu +channel-type.mybmw.mileage-channel.label = Ajettu kokonaismatka + +channel-type.mybmw.next-service-date-channel.label = Seuraava huoltopäivämäärä +channel-type.mybmw.next-service-date-channel.state.pattern = %1$tb %1$tY +channel-type.mybmw.next-service-mileage-channel.label = Kilometrit seuraavaan huoltoon +channel-type.mybmw.passenger-front-channel.label = Matkustajan ovi +channel-type.mybmw.passenger-rear-channel.label = Matkustajan puoleinen takaovi +channel-type.mybmw.plug-connection-channel.label = Pistokeyhteyden tila +channel-type.mybmw.png-channel.label = Renderoidun ajoneuvon kuva + +channel-type.mybmw.profile-climate-channel.label = Ilmastointi lähtöaikana +channel-type.mybmw.profile-control-channel.command.option.weeklyPlanner = Viikkoaikataulu +channel-type.mybmw.profile-control-channel.description = Lataussuunnitelman valinta +channel-type.mybmw.profile-control-channel.label = Lataussuunnitelma +channel-type.mybmw.profile-limit-channel.description = Rajoitettu lataus aktivoitu +channel-type.mybmw.profile-limit-channel.label = Latausenergia rajoitettu +channel-type.mybmw.profile-mode-channel.command.option.delayedCharging = Käytä latausasetusta +channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Välitön lataus +channel-type.mybmw.profile-mode-channel.description = Välittömän tai viivästetyn latauksen tilan valinta +channel-type.mybmw.profile-mode-channel.label = Lataustila +channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Latausikkuna +channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Ei valintaa +channel-type.mybmw.profile-prefs-channel.description = Viivästetyn latauksen asetukset +channel-type.mybmw.profile-prefs-channel.label = Latausasetukset +channel-type.mybmw.profile-target-channel.description = Lataustilan tavoite +channel-type.mybmw.profile-target-channel.label = Lataustilan tavoite + +channel-type.mybmw.range-electric-channel.label = Sähköinen toimintamatka +channel-type.mybmw.range-fuel-channel.label = Toimintamatka polttoaineella +channel-type.mybmw.range-hybrid-channel.label = Yhdistetty toimintamatka +channel-type.mybmw.range-radius-electric-channel.label = Sähköinen toimintasäde +channel-type.mybmw.range-radius-fuel-channel.label = Toimintasäde polttoaineella +channel-type.mybmw.range-radius-hybrid-channel.label = Yhdistetty toimintasäde +channel-type.mybmw.raw-channel.label = Raakadata + +channel-type.mybmw.rear-left-current-channel.label = Rengaspaine vasen taka +channel-type.mybmw.rear-left-target-channel.label = Rengaspaine vasen taka tavoite +channel-type.mybmw.rear-right-current-channel.label = Rengaspaine oikea taka +channel-type.mybmw.rear-right-target-channel.label = Rengaspaine oikea taka tavoite +channel-type.mybmw.remaining-fuel-channel.label = Jäljellä oleva polttoaine +channel-type.mybmw.remote-command-channel.command.option.climate-now-start = Aloita ilmastointi nyt +channel-type.mybmw.remote-command-channel.command.option.climate-now-stop = Lopeta ilmastointi nyt +channel-type.mybmw.remote-command-channel.command.option.door-lock = Lukitse ajoneuvo +channel-type.mybmw.remote-command-channel.command.option.door-unlock = Avaa ajoneuvon lukitus +channel-type.mybmw.remote-command-channel.command.option.horn-blow = Soita äänitorvea +channel-type.mybmw.remote-command-channel.command.option.light-flash = Väläytä valoja +channel-type.mybmw.remote-command-channel.command.option.vehicle-finder = Etsi ajoneuvo +channel-type.mybmw.remote-command-channel.label = Etäkomento +channel-type.mybmw.remote-state-channel.label = Etäkomenmon tila +channel-type.mybmw.service-date-channel.label = Huollon päiväys +channel-type.mybmw.service-date-channel.state.pattern = %1$tb %1$tY +channel-type.mybmw.service-details-channel.label = Huollon yksityiskohdat +channel-type.mybmw.service-mileage-channel.label = Kilometrit huoltoon asti +channel-type.mybmw.service-name-channel.label = Palvelun nimi +channel-type.mybmw.session-energy-channel.label = Ladattu energia +channel-type.mybmw.session-issue-channel.label = Ongelmia latauksen aikana +channel-type.mybmw.session-status-channel.label = Latauksen tila +channel-type.mybmw.session-subtitle-channel.label = Latauksen yksityiskohdat +channel-type.mybmw.session-title-channel.label = Latauksen nimi +channel-type.mybmw.soc-channel.label = Akun lataustaso + +channel-type.mybmw.statistic-energy-channel.description = Kuluvan kuukauden ladattu kokonaisenergia +channel-type.mybmw.statistic-energy-channel.label = Latauskustannus +channel-type.mybmw.statistic-sessions-channel.description = Latauskertojen määrä tässä kuussa +channel-type.mybmw.statistic-sessions-channel.label = Latauskerrat +channel-type.mybmw.statistic-title-channel.label = Lataustilastokuukausi +channel-type.mybmw.sunroof-channel.label = Kattoluukku + +channel-type.mybmw.timer1-day-fri-channel.description = Perjantaina ajastin 1 +channel-type.mybmw.timer1-day-fri-channel.label = T1 Perjantai +channel-type.mybmw.timer1-day-mon-channel.description = Maanantaina ajastin 1 +channel-type.mybmw.timer1-day-mon-channel.label = T1 Maanantai +channel-type.mybmw.timer1-day-sat-channel.description = Lauantaina ajastin 1 +channel-type.mybmw.timer1-day-sat-channel.label = T1 Lauantai +channel-type.mybmw.timer1-day-sun-channel.description = Sunnuntaina ajastin 1 +channel-type.mybmw.timer1-day-sun-channel.label = T1 Sunnuntai +channel-type.mybmw.timer1-day-thu-channel.description = Torstaina ajastin 1 +channel-type.mybmw.timer1-day-thu-channel.label = T1 Torstai +channel-type.mybmw.timer1-day-tue-channel.description = Tiistaina ajastin 1 +channel-type.mybmw.timer1-day-tue-channel.label = T1 Tiistai +channel-type.mybmw.timer1-day-wed-channel.description = Keskiviikkona ajastin 1 +channel-type.mybmw.timer1-day-wed-channel.label = T1 Keskiviikko +channel-type.mybmw.timer1-departure-channel.description = Lähtöaika normaalille aikatauluajastimelle 1 +channel-type.mybmw.timer1-departure-channel.label = T1 Lähtöaika +channel-type.mybmw.timer1-departure-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.timer1-enabled-channel.description = Ajastin 1 käytössä +channel-type.mybmw.timer1-enabled-channel.label = T1 käytössä +channel-type.mybmw.timer2-day-fri-channel.description = Perjantaina ajastin 2 +channel-type.mybmw.timer2-day-fri-channel.label = T2 Perjantai +channel-type.mybmw.timer2-day-mon-channel.description = Maanantaina ajastin 2 +channel-type.mybmw.timer2-day-mon-channel.label = T2 Maanantai +channel-type.mybmw.timer2-day-sat-channel.description = Lauantaina ajastin 2 +channel-type.mybmw.timer2-day-sat-channel.label = T2 Lauantai +channel-type.mybmw.timer2-day-sun-channel.description = Sunnuntaina ajastin 2 +channel-type.mybmw.timer2-day-sun-channel.label = T2 Sunnuntai +channel-type.mybmw.timer2-day-thu-channel.description = Torstaina ajastin 2 +channel-type.mybmw.timer2-day-thu-channel.label = T2 Torstai +channel-type.mybmw.timer2-day-tue-channel.description = Tiistaina ajastin 2 +channel-type.mybmw.timer2-day-tue-channel.label = T2 Tiistai +channel-type.mybmw.timer2-day-wed-channel.description = Keskiviikkona ajastin 2 +channel-type.mybmw.timer2-day-wed-channel.label = T2 Keskiviikko +channel-type.mybmw.timer2-departure-channel.description = Lähtöaika normaalille aikatauluajastimelle 2 +channel-type.mybmw.timer2-departure-channel.label = T2 Lähtöaika +channel-type.mybmw.timer2-departure-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.timer2-enabled-channel.description = Ajastin 2 käytössä +channel-type.mybmw.timer2-enabled-channel.label = T2 käytössä +channel-type.mybmw.timer3-day-fri-channel.description = Perjantaina ajastin 3 +channel-type.mybmw.timer3-day-fri-channel.label = T3 Perjantai +channel-type.mybmw.timer3-day-mon-channel.description = Maanantaina ajastin 3 +channel-type.mybmw.timer3-day-mon-channel.label = T3 Maanantai +channel-type.mybmw.timer3-day-sat-channel.description = Lauantaina ajastin 3 +channel-type.mybmw.timer3-day-sat-channel.label = T3 Lauantai +channel-type.mybmw.timer3-day-sun-channel.description = Sunnuntaina ajastin 3 +channel-type.mybmw.timer3-day-sun-channel.label = T3 Sunnuntai +channel-type.mybmw.timer3-day-thu-channel.description = Torstaina ajastin 3 +channel-type.mybmw.timer3-day-thu-channel.label = T3 Torstai +channel-type.mybmw.timer3-day-tue-channel.description = Tiistaina ajastin 3 +channel-type.mybmw.timer3-day-tue-channel.label = T3 Tiistai +channel-type.mybmw.timer3-day-wed-channel.description = Keskiviikkona ajastin 3 +channel-type.mybmw.timer3-day-wed-channel.label = T3 Keskiviikko +channel-type.mybmw.timer3-departure-channel.description = Lähtöaika normaalille aikatauluajastimelle 3 +channel-type.mybmw.timer3-departure-channel.label = T3 Lähtöaika +channel-type.mybmw.timer3-departure-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.timer3-enabled-channel.description = Ajastin 3 käytössä +channel-type.mybmw.timer3-enabled-channel.label = T3 käytössä +channel-type.mybmw.timer4-day-fri-channel.description = Perjantaina ajastin 4 +channel-type.mybmw.timer4-day-fri-channel.label = T4 Perjantai +channel-type.mybmw.timer4-day-mon-channel.description =Maanantaina ajastin 4 +channel-type.mybmw.timer4-day-mon-channel.label = T4 Maanantai +channel-type.mybmw.timer4-day-sat-channel.description = Lauantaina ajastin 4 +channel-type.mybmw.timer4-day-sat-channel.label = T4 Lauantai +channel-type.mybmw.timer4-day-sun-channel.description = Sunnuntaina ajastin 4 +channel-type.mybmw.timer4-day-sun-channel.label = T4 Sunnuntai +channel-type.mybmw.timer4-day-thu-channel.description = Torstaina ajastin 4 +channel-type.mybmw.timer4-day-thu-channel.label = T4 Torstai +channel-type.mybmw.timer4-day-tue-channel.description = Tiistaina ajastin 4 +channel-type.mybmw.timer4-day-tue-channel.label = T4 Tiistai +channel-type.mybmw.timer4-day-wed-channel.description =Keskiviikkona ajastin 4 +channel-type.mybmw.timer4-day-wed-channel.label = T4 Keskiviikko +channel-type.mybmw.timer4-departure-channel.description = Lähtöaika normaalille aikatauluajastimelle 4 +channel-type.mybmw.timer4-departure-channel.label = T4 Lähtöaika +channel-type.mybmw.timer4-departure-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.timer4-enabled-channel.description = Ajastin 4 käytössä +channel-type.mybmw.timer4-enabled-channel.label = T4 käytössä +channel-type.mybmw.trunk-channel.label = Tavaratila +channel-type.mybmw.window-driver-front-channel.label = Kuljettajan ikkuna +channel-type.mybmw.window-driver-rear-channel.label = Kuljettajan puoleinen takaikkuna +channel-type.mybmw.window-end-channel.description = Lataustapahtuman päättymisaika +channel-type.mybmw.window-end-channel.label = Latausikkunan päättymisaika +channel-type.mybmw.window-end-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.window-passenger-front-channel.label = Matkustajan ikkuna +channel-type.mybmw.window-passenger-rear-channel.label = Matkustajan puoleinen takaikkuna +channel-type.mybmw.window-start-channel.description = Lataustapahtuman alkamisaika +channel-type.mybmw.window-start-channel.label = Latauksen aloitusaika +channel-type.mybmw.window-start-channel.state.pattern = %1$tH:%1$tM +channel-type.mybmw.windows-channel.label = Ikkunoiden tila From f94b4488c0de9e67f87c4b85665807b65f8c7200 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Sun, 19 Feb 2023 22:21:58 +0100 Subject: [PATCH 04/64] [mybmw] update Copyright header Signed-off-by: Martin Grassl --- .../mybmw/internal/MyBMWVehicleConfiguration.java | 2 +- .../mybmw/internal/console/MyBMWCommandExtension.java | 2 +- .../mybmw/internal/dto/charge/ChargingProfile.java | 2 +- .../mybmw/internal/dto/charge/ChargingSession.java | 2 +- .../mybmw/internal/dto/charge/ChargingSessions.java | 2 +- .../mybmw/internal/dto/charge/ChargingStatistics.java | 2 +- .../dto/charge/ChargingStatisticsContainer.java | 2 +- .../internal/dto/charge/RemoteChargingCommands.java | 2 +- .../internal/dto/vehicle/CheckControlMessage.java | 2 +- .../mybmw/internal/dto/vehicle/ClimateTimer.java | 2 +- .../internal/dto/vehicle/CombustionFuelLevel.java | 2 +- .../mybmw/internal/dto/vehicle/Coordinates.java | 2 +- .../mybmw/internal/dto/vehicle/DepartureTime.java | 2 +- .../mybmw/internal/dto/vehicle/DigitalKey.java | 2 +- .../mybmw/internal/dto/vehicle/DriverPreferences.java | 2 +- .../internal/dto/vehicle/ElectricChargingState.java | 2 +- .../mybmw/internal/dto/vehicle/RequiredService.java | 2 +- .../mybmw/internal/dto/vehicle/VehicleAttributes.java | 2 +- .../mybmw/internal/dto/vehicle/VehicleBase.java | 2 +- .../internal/dto/vehicle/VehicleCapabilities.java | 2 +- .../mybmw/internal/dto/vehicle/VehicleDoorsState.java | 2 +- .../mybmw/internal/dto/vehicle/VehicleLocation.java | 2 +- .../mybmw/internal/dto/vehicle/VehicleRoofState.java | 2 +- .../mybmw/internal/dto/vehicle/VehicleState.java | 2 +- .../internal/dto/vehicle/VehicleStateContainer.java | 2 +- .../mybmw/internal/dto/vehicle/VehicleTireState.java | 2 +- .../internal/dto/vehicle/VehicleTireStateDetails.java | 2 +- .../VehicleTireStateDetailsClassification.java | 2 +- .../internal/dto/vehicle/VehicleTireStateStatus.java | 2 +- .../mybmw/internal/dto/vehicle/VehicleTireStates.java | 2 +- .../internal/dto/vehicle/VehicleWindowsState.java | 2 +- .../mybmw/internal/handler/RemoteServiceExecutor.java | 2 +- .../internal/handler/auth/MyBMWTokenController.java | 2 +- .../handler/backend/JsonStringDeserializer.java | 2 +- .../internal/handler/backend/MyBMWFileProxy.java | 2 +- .../internal/handler/backend/MyBMWHttpProxy.java | 2 +- .../mybmw/internal/handler/backend/MyBMWProxy.java | 2 +- .../internal/handler/backend/NetworkException.java | 2 +- .../handler/backend/ResponseContentAnonymizer.java | 2 +- .../mybmw/internal/handler/enums/RemoteService.java | 11 +++++++++-- .../mybmw/internal/utils/ChargingProfileWrapper.java | 2 +- .../internal/utils/MyBMWConfigurationChecker.java | 2 +- .../internal/discovery/VehicleDiscoveryTest.java | 2 +- .../mybmw/internal/dto/vehicle/VehicleBaseTest.java | 2 +- .../internal/dto/vehicle/VehicleCapabilitiesTest.java | 2 +- .../dto/vehicle/VehicleStateContainerTest.java | 2 +- .../handler/backend/JsonStringDeserializerTest.java | 2 +- .../internal/handler/backend/MyBMWHttpProxyTest.java | 2 +- .../internal/handler/backend/MyBMWProxyBackendIT.java | 8 +++++++- .../backend/ResponseContentAnonymizerTest.java | 2 +- .../binding/mybmw/internal/utils/ConverterTest.java | 2 +- .../internal/utils/MyBMWConfigurationCheckerTest.java | 2 +- 52 files changed, 66 insertions(+), 53 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWVehicleConfiguration.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWVehicleConfiguration.java index 1d29d45ec338b..f37b9c273e0a5 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWVehicleConfiguration.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWVehicleConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java index 3a1b5f70e9954..7cbb092b3d68f 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java index 668b32f0aa776..ab80e7d6703e0 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSession.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSession.java index 5338c9b62ceef..96019676dc077 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSession.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSession.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessions.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessions.java index 258858cf37b08..90a787523848d 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessions.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingSessions.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatistics.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatistics.java index 7816f45fd45b5..1fa27e176d7a7 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatistics.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatistics.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsContainer.java index 1055e131c05c9..e1507e2f5d9d4 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsContainer.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsContainer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/RemoteChargingCommands.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/RemoteChargingCommands.java index 1da018f3b67e0..0adfff9137c01 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/RemoteChargingCommands.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/RemoteChargingCommands.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CheckControlMessage.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CheckControlMessage.java index b6263fa34d55d..deb3ba6d2c650 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CheckControlMessage.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CheckControlMessage.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateTimer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateTimer.java index 1a0aeb7378641..2be87e8289453 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateTimer.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ClimateTimer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CombustionFuelLevel.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CombustionFuelLevel.java index 46424f2f5a594..53b91713a36d2 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CombustionFuelLevel.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/CombustionFuelLevel.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Coordinates.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Coordinates.java index ca4912fb9c968..3e0fe7d0186a2 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Coordinates.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/Coordinates.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DepartureTime.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DepartureTime.java index a9a16480def4f..5267c1cf0b1e1 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DepartureTime.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DepartureTime.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DigitalKey.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DigitalKey.java index c639efab68939..72b111f4336d8 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DigitalKey.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DigitalKey.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DriverPreferences.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DriverPreferences.java index 8ae43c4eb4175..b32297513d70c 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DriverPreferences.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/DriverPreferences.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ElectricChargingState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ElectricChargingState.java index 9706655f18d6a..e49dc0aa49057 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ElectricChargingState.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/ElectricChargingState.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RequiredService.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RequiredService.java index 7c4acea4dc4c6..604ac7539eccc 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RequiredService.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/RequiredService.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleAttributes.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleAttributes.java index 882a01f34ff02..fa8361f713b3c 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleAttributes.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleAttributes.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBase.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBase.java index b8dadbad1e506..2cbd203f1f210 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBase.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java index d58eb4eefde88..b90a87f9b1115 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleDoorsState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleDoorsState.java index 55a2febd292c2..89db70a872fb4 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleDoorsState.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleDoorsState.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java index 396bcce6d2725..04b55ff5fbc9e 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleRoofState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleRoofState.java index 8b1e002c19015..af9105e9916ca 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleRoofState.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleRoofState.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleState.java index d9bf0a755b9b9..fe0f6412ed301 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleState.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleState.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainer.java index d854998a0ece0..775d45f08e10f 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainer.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireState.java index cec5b2467e328..bd6f83f910841 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireState.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireState.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetails.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetails.java index a325edaa8b579..0a60c25d3fa9f 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetails.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetails.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetailsClassification.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetailsClassification.java index 03ad0646a1eac..75df0c7541de0 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetailsClassification.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateDetailsClassification.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateStatus.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateStatus.java index 92471802a1ab5..8cda59acd5d3d 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateStatus.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStateStatus.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStates.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStates.java index 17ca131617061..46a88994d4904 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStates.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleTireStates.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleWindowsState.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleWindowsState.java index 50fc815d0d00e..035fc427a099c 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleWindowsState.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleWindowsState.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java index fc7e6daccbbff..3025a281ac3ee 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java index bc25fceee2dee..24ae2e052e915 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java index 74b439fc6d3d6..8ee1f7dda8597 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java index 49eb5a64e5896..4dd16cf400647 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWFileProxy.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java index ff6dc38a584ff..28f24ba102539 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java index 0d3e29db3f402..a18004112c3b1 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxy.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/NetworkException.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/NetworkException.java index 5397733400152..ed5fa1f310f22 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/NetworkException.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/NetworkException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizer.java index f3107abf8ab17..d6ce50c9c7566 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizer.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java index 6b3812a6b840c..a8ecff69f97d3 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. @@ -12,7 +12,14 @@ */ package org.openhab.binding.mybmw.internal.handler.enums; -import static org.openhab.binding.mybmw.internal.MyBMWConstants.*; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_AIR_CONDITIONING_START; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_AIR_CONDITIONING_STOP; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_CHARGE; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_DOOR_LOCK; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_DOOR_UNLOCK; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_HORN; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_LIGHT_FLASH; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_VEHICLE_FINDER; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileWrapper.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileWrapper.java index 10f04174863ed..ae25430dcb3b4 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileWrapper.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/ChargingProfileWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java index 9c9dc28200e79..ff559bd2eab33 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscoveryTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscoveryTest.java index 533c32982ec5c..2bdb1fabf5962 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscoveryTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscoveryTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java index 3e66b9981c998..c5374e4f1cbab 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilitiesTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilitiesTest.java index e26baf1b271cb..6876a45aed395 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilitiesTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilitiesTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java index 42414081d0360..84e2c6ab55ee1 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java index 71478216f6b20..86668c1823ada 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java index 8a306f5647f93..b24fdbf6e033e 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java index 62c0a245b5b4d..bdbbb330f29b2 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. @@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.BeforeEach; @@ -228,4 +229,9 @@ public HttpClient createHttpClient(String consumerName) { public HttpClient getCommonHttpClient() { return createHttpClient("test"); } + + @Override + public HttpClient createHttpClient(String consumerName, @Nullable SslContextFactory sslContextFactory) { + return createHttpClient(consumerName); + } } diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizerTest.java index b1cc4206cfacf..25962d5e00435 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizerTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/ResponseContentAnonymizerTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java index c5d5c0db06a74..7ef363db4d643 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java index df90a6612363f..c5133ed0ae580 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationCheckerTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2022 Contributors to the openHAB project + * Copyright (c) 2010-2023 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. From 6e525c98d795fd0354352c32051ba87b06561a33 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 20 Feb 2023 22:28:43 +0100 Subject: [PATCH 05/64] [mybmw/Documentation] re-fix linting of README according to #13866 Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/README.md | 416 ++++++++++---------- 1 file changed, 199 insertions(+), 217 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md index 2e1fe27c79fba..2fcf845e5947e 100644 --- a/bundles/org.openhab.binding.mybmw/README.md +++ b/bundles/org.openhab.binding.mybmw/README.md @@ -1,25 +1,25 @@ # MyBMW Binding The binding provides access like [MyBMW App](https://www.bmw.com/en/footer/mybmw-app.html) to openHAB. -All vehicles connected to an account will be detected by the discovery with the correct type: +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 +- 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 +The provided data depends on -1. the [Thing Type](#things) and -2. the [Properties](#properties) mentioned in Services +1. the [Thing Type](#things) and +1. the [Properties](#properties) mentioned in Services Different channel groups are clustering all information. Check for each group if it's supported by your vehicle. -Please note **this isn't a real-time binding**. -If a door is opened the state isn't transmitted and changed immediately. -It's not a flaw in the binding itself because the state in BMW's own MyBMW App is also updated with some delay. +Please note **this isn't a real-time binding**. +If a door is opened the state isn't transmitted and changed immediately. +It's not a flaw in the binding itself because the state in BMW's own MyBMW App is also updated with some delay. ## Supported Things @@ -31,14 +31,13 @@ The bridge establishes the connection between BMW API and openHAB. |----------------------------|----------------|------------------------------------------| | MyBMW Account | `account` | Access to BMW API for a specific user | - ### Things -Four different vehicle types are provided. -They differ in the supported channel groups & channels. -Conventional Fuel Vehicles don't provide e.g. _Charging Profile_, Electric Vehicles don't provide a _Fuel Range_. +Four different vehicle types are provided. +They differ in the supported channel groups & channels. +Conventional Fuel Vehicles don't provide e.g. _Charging Profile_, Electric Vehicles don't provide a _Fuel Range_. For hybrid vehicles in addition to _Fuel and Electric Range_ the _Hybrid Range_ is shown. - + | Name | Thing Type ID | Supported Channel Groups | |-------------------------------------|---------------|---------------------------------------------------------------------| | BMW Electric Vehicle | `bev` | Vehicle with electric drive train | @@ -46,21 +45,20 @@ For hybrid vehicles in addition to _Fuel and Electric Range_ the _Hybrid Range_ | BMW Plug-In-Hybrid Electric Vehicle | `phev` | Vehicle with combustion and electric drive train | | BMW Conventional Vehicle | `conv` | Vehicle with combustion drive train | - #### Properties -For each vehicle properties are available. +For each vehicle properties are available. Basic information is given regarding -* Vehicle properties like model type, drive train and construction year -* Which services are available / not available +- Vehicle properties like model type, drive train and construction year +- Which services are available / not available -In the right picture can see in *remoteServicesEnabled* e.g. the *Door Lock* and *Door Unlock* services are mentioned. +In the right picture can see in _remoteServicesEnabled_ e.g. the _Door Lock_ and _Door Unlock_ services are mentioned. This ensures channel group [Remote Services](#remote-services) is supporting door lock and unlock remote control. -In *Services Supported* the entry *ChargingHistory* is mentioned. +In _Services Supported_ the entry _ChargingHistory_ is mentioned. So it's valid to connect channel group [Charge Sessions](#charge-sessions) in order to display your last charging sessions. | Property Key | Property Value | Supported Channel Groups | @@ -68,12 +66,11 @@ So it's valid to connect channel group [Charge Sessions](#charge-sessions) in or | servicesSupported | ChargingHistory | session | | remoteServicesEnabled | _list of services_ | remote | - ## Discovery -Auto discovery is starting after the bridge is created. +Auto discovery is starting after the bridge is created. A list of your registered vehicles is queried and all found things are added in the inbox. -Unique identifier is the *Vehicle Identification Number* (VIN). +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. @@ -81,7 +78,7 @@ Properties will be attached to predefined vehicles if the VIN is matching. ### Bridge Configuration -| Parameter | Type | Description | +| Parameter | Type | Description | |-----------------|---------|--------------------------------------------------------------------| | userName | text | MyBMW Username | | password | text | MyBMW Password | @@ -89,53 +86,49 @@ Properties will be attached to predefined vehicles if the VIN is matching. The region Configuration has 3 different options -* _NORTH_AMERICA_ -* _CHINA_ -* _ROW_ (Rest of World) - +- _NORTH_AMERICA_ +- _CHINA_ +- _ROW_ (Rest of World) #### Advanced Configuration -| Parameter | Type | Description | +| Parameter | Type | Description | |-----------------|---------|---------------------------------------------------------| | language | text | Channel data can be returned in the desired language | -Language is predefined as *AUTODETECT*. +Language is predefined as _AUTODETECT_. Some textual descriptions, date and times are delivered based on your local language. You can overwrite this setting with lowercase 2-letter [language code reagrding ISO 639](https://www.oracle.com/java/technologies/javase/jdk8-jre8-suported-locales.html) -So if want your UI in english language place *en* as desired language. +So if want your UI in english language place _en_ as desired language. ### Thing Configuration Same configuration is needed for all things -| Parameter | Type | Description | +| Parameter | Type | Description | |-----------------|---------|---------------------------------------| | vin | text | Vehicle Identification Number (VIN) | | refreshInterval | integer | Refresh Interval in Minutes | - #### Advanced Configuration -| Parameter | Type | Description | +| Parameter | Type | Description | |-----------------|---------|-----------------------------------| | vehicleBrand | text | Vehicle Brand like BMW or Mini | The _vehicleBrand_ is automatically obtained by the discovery service and shall not be changed. If thing is defined manually via *.things file following brands are supported -* BMW -* MINI - +- BMW +- MINI ## Channels -There are many channels available for each vehicle. +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 +### Thing Channel Groups | Channel Group ID | Description | conv | phev | bev_rex | bev | |----------------------------------|---------------------------------------------------|------|------|---------|-----| @@ -152,14 +145,13 @@ They differ for each vehicle type, build-in sensors and activated services. | [tires](#tire-pressure) | Current and wanted pressure for all tires | X | X | X | X | | [image](#image) | Provides an image of your vehicle | X | X | X | X | - #### Vehicle Status Reflects overall status of the vehicle. -* Channel Group ID is **status** -* Available for all vehicles -* Read-only values +- Channel Group ID is **status** +- Available for all vehicles +- Read-only values | Channel Label | Channel ID | Type | Description | conv | phev | bev_rex | bev | |---------------------------|---------------------|---------------|------------------------------------------------|------|------|---------|-----| @@ -172,49 +164,49 @@ Reflects overall status of the vehicle. | Plug Connection Status | plug-connection | String | Plug is _Connected_ or _Not connected_ | | X | X | X | | Charging Status | charge | String | Current charging status | | X | X | X | | Remaining Charging Time | charge-remaining | Number:Time | Remainining time for current charging session | | X | X | X | -| Last Status Timestamp | last-update | DateTime | Date and time of last status update of the car | X | X | X | X | +| Last Status Timestamp | last-update | DateTime | Date and time of last status update | X | X | X | X | | Last Fetched Timestamp | last-fetched | DateTime | Date and time of last time status fetched | X | X | X | X | Overall Door Status values -* _Closed_ - all doors closed -* _Open_ - at least one door is open -* _Undef_ - no door data delivered at all +- _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 +- _Closed_ - all windows closed +- _Open_ - at least one window is completely open +- _Intermediate_ - at least one window is partially open +- _Undef_ - no window data delivered at all Check Control values Localized String of current active warnings. Examples: -* No Issues -* Multiple Issues +- No Issues +- Multiple Issues Charging Status values -* _Not Charging_ -* _Charging_ -* _Plugged In_ -* _Fully Charged_ +- _Not Charging_ +- _Charging_ +- _Plugged In_ +- _Fully Charged_ Charging Information values Localized String of current active charging session Examples -* 100% at ~00:43 -* Starts at ~09:00 +- 100% at ~00:43 +- Starts at ~09:00 ##### Vehicle Status Raw Data The _raw data channel_ is marked as _advanced_ and isn't shown by default. Target are advanced users to derive even more data out of BMW API replies. -As the replies are formatted as JSON use the [JsonPath Transformation Service](https://www.openhab.org/addons/transformations/jsonpath/) to extract data for an item, +As the replies are formatted as JSON use the [JsonPath Transformation Service](https://www.openhab.org/addons/transformations/jsonpath/) to extract data for an item, | Channel Label | Channel ID | Type | Description | |---------------------------|---------------------|---------------|------------------------------------------------| @@ -224,53 +216,52 @@ As the replies are formatted as JSON use the [JsonPath Transformation Service](h Examples: -_Country ISO Code_ +###### Country ISO Code -``` +```json $.properties.originCountryISO ``` -_Drivers Guide URL_ +###### Drivers Guide URL -``` +```json $.driverGuideInfo.androidStoreUrl ``` #### Range Data -Based on vehicle type some channels are present or not. -Conventional fuel vehicles don't provide *Electric Range* and battery electric vehicles don't show *Fuel Range*. -Hybrid vehicles have both and in addition *Hybrid Range*. +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 Group ID is **range** +- Availability according to table +- Read-only values | Channel Label | Channel ID | Type | conv | phev | bev_rex | bev | |------------------------------------|----------------------------|----------------------|------|------|---------|-----| | Mileage | mileage | Number:Length | X | X | X | X | | Fuel Range | range-fuel | Number:Length | X | X | X | | -| Electric Range | range-electric | Number:Length | | X | X | X | -| Hybrid Range | range-hybrid | Number:Length | | X | X | | +| Electric Range | range-electric | Number:Length | | X | X | X | +| Hybrid Range | range-hybrid | Number:Length | | X | X | | | Battery Charge Level | soc | Number:Dimensionless | | X | X | X | -| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | | -| Estimated Fuel Consumption l/100km | estimated-fuel-l-100km | Number:Dimensionless | X | X | X | | -| Estimated Fuel Consumption mpg | estimated-fuel-mpg | Number:Dimensionless | 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 | | - +| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | | +| Estimated Fuel Consumption l/100km | estimated-fuel-l-100km | Number:Dimensionless | X | X | X | | +| Estimated Fuel Consumption mpg | estimated-fuel-mpg | Number:Dimensionless | X | X | X | | +| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | | +| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X | +| Hybrid Range Radius | range-radius-hybrid | Number:Length | | X | X | | #### Doors Details Detailed status of all doors and windows. -* Channel Group ID is **doors** -* Available for all vehicles if corresponding sensors are built-in -* Read-only values - -| Channel Label | Channel ID | Type | +- 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 | @@ -287,21 +278,20 @@ Detailed status of all doors and windows. 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 - +- _Undef_ - no status data available +- _Invalid_ - this door / window isn't applicable for this vehicle +- _Closed_ - the door / window is closed +- _Open_ - the door / window is open +- _Intermediate_ - window in intermediate position, not applicable for doors #### Check Control Group for all current active Check Control messages. If more than one message is active the channel _name_ contains all active messages as options. -* Channel Group ID is **check** -* Available for all vehicles -* Read/Write access +- Channel Group ID is **check** +- Available for all vehicles +- Read/Write access | Channel Label | Channel ID | Type | Access | |---------------------------------|---------------------|----------------|------------| @@ -311,19 +301,18 @@ If more than one message is active the channel _name_ contains all active messag Severity Levels -* Ok -* Low -* Medium - +- Ok +- Low +- Medium #### Services Group for all upcoming services with description, service date and/or service mileage. If more than one service is scheduled in the future the channel _name_ contains all future services as options. -* Channel Group ID is **service** -* Available for all vehicles -* Read/Write access +- Channel Group ID is **service** +- Available for all vehicles +- Read/Write access | Channel Label | Channel ID | Type | Access | |--------------------------------|---------------------|----------------|------------| @@ -332,33 +321,31 @@ If more than one service is scheduled in the future the channel _name_ contains | Service Date | date | DateTime | Read | | Mileage till Service | mileage | Number:Length | Read | - #### Location GPS location and heading of the vehicle. -* Channel Group ID is **location** -* Available for all vehicles with built-in GPS sensor. Function can be enabled/disabled in the head unit -* Read-only values +- Channel 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 | +| Channel Label | Channel ID | Type | |---------------------|---------------------|---------------| -| GPS Coordinates | gps | Location | -| Heading | heading | Number:Angle | -| Address | address | String | -| Distance from Home | home-distance | Number:Length | +| GPS Coordinates | gps | Location | +| Heading | heading | Number:Angle | +| Address | address | String | +| Distance from Home | home-distance | Number:Length | #### Remote Services -Remote control of the vehicle. -Send a *command* to the vehicle and the *state* is reporting the execution progress. +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 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 | |-------------------------|---------------------|---------|--------| @@ -367,86 +354,83 @@ Parallel execution isn't supported. The channel _command_ provides options -* _light-flash_ -* _vehicle-finder_ -* _door-lock_ -* _door-unlock_ -* _horn-blow_ -* _climate-now-start_ -* _climate-now-stop_ -* _charge-now_ +- _light-flash_ +- _vehicle-finder_ +- _door-lock_ +- _door-unlock_ +- _horn-blow_ +- _climate-now-start_ +- _climate-now-stop_ +- _charge-now_ The channel _state_ shows the progress of the command execution in the following order -1) _initiated_ -2) _pending_ -3) _delivered_ -4) _executed_ - +1. _initiated_ +1. _pending_ +1. _delivered_ +1. _executed_ #### Charge Profile Charging options with date and time for preferred time windows and charging modes. -* Channel Group ID is **profile** -* Available for electric and hybrid vehicles -* Read access for UI. -* There are 4 timers *T1, T2, T3 and T4* available. Replace *X* with number 1,2 or 3 to target the correct timer - -| Channel Label | Channel ID | Type | -|----------------------------|---------------------------|----------| -| Charge Mode | mode | String | -| Charge Preferences | prefs | String | -| Charging Plan | control | String | -| SoC Target | target | String | -| Charging Energy Limited | limit | Switch | -| Window Start Time | window-start | DateTime | -| Window End Time | window-end | DateTime | -| A/C at Departure | climate | Switch | -| T*X* Enabled | timer*X*-enabled | Switch | -| T*X* Departure Time | timer*X*-departure | DateTime | -| T*X* Monday | timer*X*-day-mon | Switch | -| T*X* Tuesday | timer*X*-day-tue | Switch | -| T*X* Wednesday | timer*X*-day-wed | Switch | -| T*X* Thursday | timer*X*-day-thu | Switch | -| T*X* Friday | timer*X*-day-fri | Switch | -| T*X* Saturday | timer*X*-day-sat | Switch | -| T*X* Sunday | timer*X*-day-sun | Switch | +- Channel Group ID is **profile** +- Available for electric and hybrid vehicles +- Read access for UI. +- There are 4 timers _T1, T2, T3 and T4_ available. Replace _X_ with number 1,2 or 3 to target the correct timer + +| Channel Label | Channel ID | Type | +|----------------------------|---------------------------|----------| +| Charge Mode | mode | String | +| Charge Preferences | prefs | String | +| Charging Plan | control | String | +| SoC Target | target | String | +| Charging Energy Limited | limit | Switch | +| Window Start Time | window-start | DateTime | +| Window End Time | window-end | DateTime | +| A/C at Departure | climate | Switch | +| T_X_ Enabled | timer_X_-enabled | Switch | +| T_X_ Departure Time | timer_X_-departure | DateTime | +| T_X_ Monday | timer_X_-day-mon | Switch | +| T_X_ Tuesday | timer_X_-day-tue | Switch | +| T_X_ Wednesday | timer_X_-day-wed | Switch | +| T_X_ Thursday | timer_X_-day-thu | Switch | +| T_X_ Friday | timer_X_-day-fri | Switch | +| T_X_ Saturday | timer_X_-day-sat | Switch | +| T_X_ Sunday | timer_X_-day-sun | Switch | The channel _profile-mode_ supports -* *immediateCharging* -* *delayedCharging* +- _immediateCharging_ +- _delayedCharging_ The channel _profile-prefs_ supports -* *noPreSelection* -* *chargingWindow* - +- _noPreSelection_ +- _chargingWindow_ #### Charge Statistics Shows charge statistics of the current month -* Channel Group ID is **statistic** -* Available for electric and hybrid vehicles -* Read-only values - -| Channel Label | Channel ID | Type | +- Channel Group ID is **statistic** +- Available for electric and hybrid vehicles +- Read-only values + +| Channel Label | Channel ID | Type | |----------------------------|-------------------------|----------------| | Charge Statistic Month | title | String | | Energy Charged | energy | Number:Energy | | Charge Sessions | sessions | Number | - #### Charge Sessions Group for past charging sessions. If more than one message is active the channel _name_ contains all active messages as options. -* Channel Group ID is **session** -* Available for electric and hybrid vehicles -* Read-only values +- Channel Group ID is **session** +- Available for electric and hybrid vehicles +- Read-only values | Channel Label | Channel ID | Type | |---------------------------------|--------------|----------| @@ -456,16 +440,15 @@ If more than one message is active the channel _name_ contains all active messag | Issues during Session | issue | String | | Session Status | status | String | - #### Tire Pressure Current and target tire pressure values -* Channel Group ID is **tires** -* Available for all vehicles if corresponding sensors are built-in -* Read-only values - -| Channel Label | Channel ID | Type | +- Channel Group ID is **tires** +- Available for all vehicles if corresponding sensors are built-in +- Read-only values + +| Channel Label | Channel ID | Type | |----------------------------|-------------------------|------------------| | Front Left | fl-current | Number:Pressure | | Front Left Target | fl-target | Number:Pressure | @@ -476,14 +459,13 @@ Current and target tire pressure values | Rear Right | rr-current | Number:Pressure | | Rear Right Target | rr-target | Number:Pressure | - #### Image -Image representation of the vehicle. +Image representation of the vehicle. -* Channel Group ID is **image** -* Available for all vehicles -* Read/Write access +- Channel Group ID is **image** +- Available for all vehicles +- Read/Write access | Channel Label | Channel ID | Type | Access | |----------------------------|---------------------|--------|----------| @@ -492,12 +474,11 @@ Image representation of the vehicle. Possible view ports: -* _VehicleStatus_ Front Left Side View -* _FrontView_ Front View -* _FrontLeft_ Front Left Side View -* _FrontRight_ Front Right Side View -* _RearView_ Rear View - +- _VehicleStatus_ Front Left Side View +- _FrontView_ Front View +- _FrontLeft_ Front Left Side View +- _FrontRight_ Front Right Side View +- _RearView_ Rear View ## Further Descriptions @@ -507,34 +488,36 @@ Possible view ports: There are 3 occurrences of dynamic data delivered -* Upcoming Services delivered in group [Services](#services) -* Check Control Messages delivered in group [Check Control](#check-control) -* Charging Session data delivered in group [Charge Sessions](#charge-sessions) +- Upcoming Services delivered in group [Services](#services) +- Check Control Messages delivered in group [Check Control](#check-control) +- Charging Session data delivered in group [Charge Sessions](#charge-sessions) -The channel id _name_ shows the first element as default. -All other possibilities are attached as options. -The picture on the right shows the _Session Title_ item and 3 possible options. -Select the desired service and the corresponding Charge Session with _Energy Charged_, _Session Status_ and _Session Issues_ will be shown. +The channel id _name_ shows the first element as default. +All other possibilities are attached as options. +The picture on the right shows the _Session Title_ item and 3 possible options. +Select the desired service and the corresponding Charge Session with _Energy Charged_, _Session Status_ and +_Session Issues_ will be shown. ### TroubleShooting BMW has a high range of vehicles supported by their API. -In case of any issues with this binding help to resolve it! +In case of any issues with this binding help to resolve it! Please perform the following steps: -* Can you log into MyBMW App with your credentials? -* Is the vehicle listed in your account? -* Is the [MyBMW Brige](#bridge) status _Online_? +- Can you log into MyBMW App with your credentials? +- Is the vehicle listed in your account? +- Is the [MyBMW Brige](#bridge) status _Online_? -If these preconditions are fulfilled proceed with the fingerprint generation. +If these preconditions are fulfilled proceed with the fingerprint generation. #### Generate Debug Fingerprint Login to the openHAB console and use the `mybmw fingerprint` command. -Fingerprint information on your account and vehicle(s) will show in the console and can be copied from there. -A zip file with fingerprint information for your vehicle(s) will also be generated and put in the `mybmw` folder in your home directory. -This fingerprint information is valuable for the developers to better support your vehicle in the software. +Fingerprint information on your account and vehicle(s) will show in the console and can be copiedfrom there. +A zip file with fingerprint information for your vehicle(s) will also be generated and put in the `mybmw` +folder in your home directory. +This fingerprint information is valuable for the developers to better support your vehicle in thesoftware. You can restrict the accounts and vehicles for the fingerprint generation. Full syntax is available through the `mybmw help` console command. @@ -542,8 +525,8 @@ Full syntax is available through the `mybmw help` console command. Personal data is eliminated from fingerprints so it should be possible to share them in public. Data like -* Vehicle Identification Number (VIN) -* Location data +- Vehicle Identification Number (VIN) +- Location data are anonymized in the JSON response and URL's. @@ -565,30 +548,30 @@ As with fingerprint data, personal data is eliminated from logs. -You will observe differences in the vehicle range and range radius values. +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. +The right picture shows the distance between Kassel and Frankfurt in Germany. While the air-line distance is 145 kilometers the route distance is 192 kilometers. So range value is the normal remaining range while the range radius values can be used e.g. on [Mapview](https://www.openhab.org/docs/ui/sitemaps.html#element-type-mapview) to indicate the reachable range on map. Please note this is just an indicator of the effective range. -Especially for electric vehicles it depends on many factors like driving style and usage of electric consumers. +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). +The example is based on a BMW i3 with range extender (REX). Exchange configuration parameters in the Things section -* 4711 - any id you want -* YOUR_USERNAME - with your MyBMW login username -* YOUR_PASSWORD - with your MyBMW password credentials -* VEHICLE_VIN - the vehicle identification number +- 4711 - any id you want +- YOUR_USERNAME - with your MyBMW login username +- YOUR_PASSWORD - with your MyBMW password credentials +- VEHICLE_VIN - the vehicle identification number -In addition search for all occurrences of *i3* and replace it with your Vehicle Identification like *x3* or *535d* and you're ready to go! +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 -``` +```java Bridge mybmw:account:4711 "MyBMW Account" [userName="YOUR_USERNAME",password="YOUR_PASSWORD",region="ROW"] { Thing bev_rex i3 "BMW i3 94h REX" [ vin="VEHICLE_VIN",refreshInterval=5,vehicleBrand="BMW"] } @@ -596,7 +579,7 @@ Bridge mybmw:account:4711 "MyBMW Account" [userName="YOUR_USERNAME",password=" ### Items File -``` +```java Number:Length i3Mileage "Odometer [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#mileage" } Number:Length i3Range "Range [%d %unit%]" (i3) {channel="mybmw:bev_rex:4711:i3:range#hybrid"} Number:Length i3RangeElectric "Electric Range [%d %unit%]" (i3,long) {channel="mybmw:bev_rex:4711:i3:range#electric"} @@ -717,7 +700,7 @@ String i3ImageViewport "Image Viewport [%s]" ### Sitemap File -``` +```perl sitemap BMW label="BMW" { Frame label="BMW i3" { Image item=i3Image @@ -849,5 +832,4 @@ sitemap BMW label="BMW" { ## Credits -This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected). - +This work is based on the project of [Bimmer Connected](https://github.com/bimmerconnected/bimmer_connected). From 0227b6a55ee527e6b6132d99053da14688bf0e8f Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 20 Mar 2023 21:49:27 +0100 Subject: [PATCH 06/64] [mybmw] add and replace code owners Also-by: Mark Herwege Signed-off-by: Martin Grassl --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index f1f204fa412cd..4a1bfac2731ad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -216,7 +216,7 @@ /bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff @antroids /bundles/org.openhab.binding.mqtt.homie/ @davidgraeff /bundles/org.openhab.binding.mycroft/ @dalgwen -/bundles/org.openhab.binding.mybmw/ @weymann @ntruchsess +/bundles/org.openhab.binding.mybmw/ @ntruchsess @mherwege @martingrassl /bundles/org.openhab.binding.myq/ @digitaldan /bundles/org.openhab.binding.mystrom/ @pail23 /bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn From 2a36347f2eb2d46930f33d7b70a608bbc7a3298c Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 20 Mar 2023 21:53:57 +0100 Subject: [PATCH 07/64] [mybmw] add and replace code owners - fix typo Also-by: Mark Herwege Signed-off-by: Martin Grassl --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index a7749c8210bd4..00a87e38c4dd4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -216,7 +216,7 @@ /bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff @antroids /bundles/org.openhab.binding.mqtt.homie/ @davidgraeff /bundles/org.openhab.binding.mycroft/ @dalgwen -/bundles/org.openhab.binding.mybmw/ @ntruchsess @mherwege @martingrassl +/bundles/org.openhab.binding.mybmw/ @ntruchsess @mherwege @mgrassl /bundles/org.openhab.binding.mynice/ @clinique /bundles/org.openhab.binding.myq/ @digitaldan /bundles/org.openhab.binding.mystrom/ @pail23 From eb3d5f846f2d486e36e28996c465908bc12cef8e Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 20 Mar 2023 21:58:35 +0100 Subject: [PATCH 08/64] [mybmw] add and replace code owners - fix name sorry for the confusion - GitHub showed an error unknown user... Also-by: Mark Herwege Signed-off-by: Martin Grassl --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 00a87e38c4dd4..a7749c8210bd4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -216,7 +216,7 @@ /bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff @antroids /bundles/org.openhab.binding.mqtt.homie/ @davidgraeff /bundles/org.openhab.binding.mycroft/ @dalgwen -/bundles/org.openhab.binding.mybmw/ @ntruchsess @mherwege @mgrassl +/bundles/org.openhab.binding.mybmw/ @ntruchsess @mherwege @martingrassl /bundles/org.openhab.binding.mynice/ @clinique /bundles/org.openhab.binding.myq/ @digitaldan /bundles/org.openhab.binding.mystrom/ @pail23 From de4e1ae849f9903ca0ddd9e88e32f88fb47891c3 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 21 Mar 2023 21:58:17 +0100 Subject: [PATCH 09/64] [mybmw] delete unit test due to compile errors new Jetty lib not yet available in jfrog Signed-off-by: Martin Grassl --- .../handler/backend/MyBMWProxyBackendIT.java | 237 ------------------ 1 file changed, 237 deletions(-) delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java deleted file mode 100644 index bdbbb330f29b2..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWProxyBackendIT.java +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mybmw.internal.handler.backend; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; - -import java.util.List; - -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration; -import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer; -import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer; -import org.openhab.binding.mybmw.internal.dto.remote.ExecutionStatusContainer; -import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; -import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleBase; -import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer; -import org.openhab.binding.mybmw.internal.handler.enums.ExecutionState; -import org.openhab.binding.mybmw.internal.handler.enums.RemoteService; -import org.openhab.binding.mybmw.internal.utils.BimmerConstants; -import org.openhab.binding.mybmw.internal.utils.ImageProperties; -import org.openhab.core.io.net.http.HttpClientFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; - -import ch.qos.logback.classic.Level; - -/** - * this integration test runs only if the connected account is set via environment variables - * CONNECTED_USER - * CONNECTED_PASSWORD - * - * if you want to execute the tests, please set the env variables and remove the disabled annotation - * - * @author Martin Grassl - initial contribution - */ -@NonNullByDefault -public class MyBMWProxyBackendIT { - - private final Logger logger = LoggerFactory.getLogger(MyBMWProxyBackendIT.class); - - public MyBMWHttpProxy initializeProxy() { - String connectedUser = System.getenv("CONNECTED_USER"); - String connectedPassword = System.getenv("CONNECTED_PASSWORD"); - assertNotNull(connectedUser); - assertNotNull(connectedPassword); - - MyBMWBridgeConfiguration configuration = new MyBMWBridgeConfiguration(); - configuration.language = "en"; - configuration.region = BimmerConstants.REGION_ROW; - configuration.userName = connectedUser; - configuration.password = connectedPassword; - - return new MyBMWHttpProxy(new MyHttpClientFactory(), configuration); - } - - @BeforeEach - public void setupLogger() { - Logger root = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); - - ((ch.qos.logback.classic.Logger) root).setLevel(Level.DEBUG); - - logger.trace("tracing enabled"); - logger.debug("debugging enabled"); - logger.info("info enabled"); - } - - @Test - public void testSequence() { - MyBMWHttpProxy myBMWProxy = initializeProxy(); - - // get list of vehicles - List<@NonNull VehicleBase> vehicles = null; - try { - vehicles = myBMWProxy.requestVehiclesBase(); - } catch (NetworkException e) { - fail(e.getReason(), e); - } - - assertNotNull(vehicles); - assertEquals(2, vehicles.size()); - - for (VehicleBase vehicleBase : vehicles) { - assertNotNull(vehicleBase.getVin()); - assertNotNull(vehicleBase.getAttributes().getBrand()); - - // get image - try { - byte[] bmwImage = myBMWProxy.requestImage(vehicleBase.getVin(), vehicleBase.getAttributes().getBrand(), - new ImageProperties()); - - assertNotNull(bmwImage); - } catch (NetworkException e) { - fail(e.getReason(), e); - } - - // get state - VehicleStateContainer vehicleState = null; - try { - vehicleState = myBMWProxy.requestVehicleState(vehicleBase.getVin(), - vehicleBase.getAttributes().getBrand()); - } catch (NetworkException e) { - fail(e.getReason(), e); - } - assertNotNull(vehicleState); - - // get charge statistics -> only successful for electric vehicles - ChargingStatisticsContainer chargeStatisticsContainer = null; - try { - chargeStatisticsContainer = myBMWProxy.requestChargeStatistics(vehicleBase.getVin(), - vehicleBase.getAttributes().getBrand()); - assertNotNull(chargeStatisticsContainer); - } catch (NetworkException e) { - logger.trace("error: {}", e.toString()); - } - - ChargingSessionsContainer chargeSessionsContainer = null; - try { - chargeSessionsContainer = myBMWProxy.requestChargeSessions(vehicleBase.getVin(), - vehicleBase.getAttributes().getBrand()); - assertNotNull(chargeSessionsContainer); - } catch (NetworkException e) { - logger.trace("error: {}", e.toString()); - } - - ExecutionStatusContainer remoteExecutionResponse = null; - try { - remoteExecutionResponse = myBMWProxy.executeRemoteServiceCall(vehicleBase.getVin(), - vehicleBase.getAttributes().getBrand(), RemoteService.LIGHT_FLASH); - } catch (NetworkException e) { - fail(e.getReason(), e); - } - - assertNotNull(remoteExecutionResponse); - logger.warn("{}", remoteExecutionResponse.toString()); - - ExecutionStatusContainer remoteExecutionStatusResponse = null; - try { - remoteExecutionStatusResponse = myBMWProxy.executeRemoteServiceStatusCall( - vehicleBase.getAttributes().getBrand(), remoteExecutionResponse.getEventId()); - - assertNotNull(remoteExecutionStatusResponse); - logger.warn("{}", remoteExecutionStatusResponse.toString()); - - int counter = 0; - while (!ExecutionState.EXECUTED.toString().equals(remoteExecutionStatusResponse.getEventStatus()) - && counter++ < 10) { - remoteExecutionStatusResponse = myBMWProxy.executeRemoteServiceStatusCall( - vehicleBase.getAttributes().getBrand(), remoteExecutionResponse.getEventId()); - logger.warn("{}", remoteExecutionStatusResponse.toString()); - - Thread.sleep(5000); - } - } catch (NetworkException e) { - fail(e.getReason(), e); - } catch (InterruptedException e) { - fail(e.getMessage(), e); - } - } - } - - @Test - @Disabled - public void testGetVehicles() { - MyBMWHttpProxy myBMWProxy = initializeProxy(); - - try { - List<@NonNull Vehicle> vehicles = myBMWProxy.requestVehicles(); - - logger.warn(ResponseContentAnonymizer.anonymizeResponseContent(new Gson().toJson(vehicles))); - assertNotNull(vehicles); - assertEquals(2, vehicles.size()); - } catch (NetworkException e) { - fail(e.getReason(), e); - } - } -} - -/** - * @author Martin Grassl - initial contribution - */ -@NonNullByDefault -class MyHttpClientFactory implements HttpClientFactory { - - private final Logger logger = LoggerFactory.getLogger(MyHttpClientFactory.class); - - @Override - public HttpClient createHttpClient(String consumerName) { - // Instantiate and configure the SslContextFactory - SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); - - // Instantiate HttpClient with the SslContextFactory - HttpClient httpClient = new HttpClient(sslContextFactory); - - // Configure HttpClient, for example: - httpClient.setFollowRedirects(false); - - // Start HttpClient - try { - httpClient.start(); - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - - return httpClient; - } - - @Override - public HttpClient getCommonHttpClient() { - return createHttpClient("test"); - } - - @Override - public HttpClient createHttpClient(String consumerName, @Nullable SslContextFactory sslContextFactory) { - return createHttpClient(consumerName); - } -} From f517c4d6008f48a454983cf2b184944aea014f07 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 21 Mar 2023 22:14:26 +0100 Subject: [PATCH 10/64] [mybmw] fix spotless error Signed-off-by: Martin Grassl --- .../mybmw/internal/dto/vehicle/VehicleStateContainerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java index 84e2c6ab55ee1..db55983dfbab3 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleStateContainerTest.java @@ -51,7 +51,7 @@ public void testVehicleStateDeserializationByConverter() { assertEquals("2024-06-01T00:00", ((DateTimeType) VehicleStatusUtils .getNextServiceDate(vehicleStateContainer.getState().getRequiredServices())).getZonedDateTime() - .toLocalDateTime().toString(), + .toLocalDateTime().toString(), "Service Date"); assertEquals("2022-12-21T15:41:23Z", vehicleStateContainer.getState().getLastUpdatedAt(), "Last update time"); From 25ab42fa0c58c5249f1027bc52415bbbaeab59fd Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 28 Mar 2023 22:06:23 +0200 Subject: [PATCH 11/64] [mybmw] apply change requested by @lsiepel Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md index 2fcf845e5947e..beb186b2d88d8 100644 --- a/bundles/org.openhab.binding.mybmw/README.md +++ b/bundles/org.openhab.binding.mybmw/README.md @@ -536,11 +536,7 @@ Your feedback is highly appreciated! #### Debug Logging -You can [enable debug logging](https://www.openhab.org/docs/administration/logging.html#defining-what-to-log) to get more information on the behaviour of the binding. - -``` -log:set DEBUG org.openhab.binding.mybmw -``` +You can [enable debug logging](https://www.openhab.org/docs/administration/logging.html#defining-what-to-log) to get more information on the behaviour of the binding. The package.subpackage in this case would be "org.openhab.binding.mybmw". As with fingerprint data, personal data is eliminated from logs. From 41aa52f42d1e363cbf79f7cc421dfc6578cedb5d Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Thu, 17 Aug 2023 22:03:15 +0200 Subject: [PATCH 12/64] [mybmw] fix UoM issues Signed-off-by: Martin Grassl --- .../internal/handler/VehicleHandler.java | 3 ++- .../resources/OH-INF/i18n/mybmw_de.properties | 2 +- .../OH-INF/thing/location-channel-types.xml | 2 +- .../OH-INF/thing/profile-channel-types.xml | 3 ++- .../OH-INF/thing/range-channel-types.xml | 20 +++++++++---------- .../OH-INF/thing/service-channel-types.xml | 2 +- .../OH-INF/thing/tires-channel-types.xml | 16 +++++++-------- .../thing/vehicle-status-channel-types.xml | 2 +- 8 files changed, 26 insertions(+), 24 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java index 19fb1829cd54c..3e6a9b6c65146 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java @@ -744,7 +744,8 @@ private void updateChargingProfile(ChargingProfile cp, @Nullable String channelT ChargingSettings cs = cpw.getChargingSettings(); if (cs != null) { updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_TARGET, - DecimalType.valueOf(Integer.toString(cs.getTargetSoc())), channelToBeUpdated); + QuantityType.valueOf(cs.getTargetSoc(), Units.PERCENT), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_LIMIT, OnOffType.from(cs.isAcCurrentLimitActive()), channelToBeUpdated); } diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties index 9d9ba4307be01..a76faae3d6f5c 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties @@ -124,7 +124,7 @@ channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Keine P channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Laden im Zeitfenster channel-type.mybmw.profile-target-channel.label = Ziel Ladezustand channel-type.mybmw.profile-target-channel.description = Erwünschter Batterie Ladezustand -channel-type.mybmw.range-electric-channel.label = Elektrische Reichweite +channel-type.mybmw.range-electric-channel.label = Elektrische Reichweite channel-type.mybmw.range-fuel-channel.label = Verbrenner Reichweite channel-type.mybmw.range-hybrid-channel.label = Hybride Reichweite channel-type.mybmw.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml index df395e8a4cf01..28959b3776253 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml @@ -20,6 +20,6 @@ Number:Length Computed distance between vehicle and home location - + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml index c677f62056eda..3341a943d3526 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/profile-channel-types.xml @@ -40,9 +40,10 @@ - Number + Number:Dimensionless SOC charging target + Switch diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml index c3246232a256e..1b9a5eb1a7a4b 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml @@ -6,32 +6,32 @@ Number:Length - + Number:Length - + Number:Length - + Number:Length - + Number:Dimensionless - + Number:Volume - + Number:Dimensionless @@ -40,22 +40,22 @@ Number:Dimensionless - + Number:Length - + Number:Length - + Number:Length - + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml index 827dd8a808b83..ac2727b6b8c8c 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml @@ -19,6 +19,6 @@ Number:Length - + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml index e403c049fafd7..361c9d4bb5eb0 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml @@ -6,41 +6,41 @@ Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml index cc0bf7f5d97a9..5ffe5974cbda8 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml @@ -25,7 +25,7 @@ Number:Length - + String From 3d73f5217ade72322c3c15f461ee50c3476ab88e Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Fri, 18 Aug 2023 22:27:55 +0200 Subject: [PATCH 13/64] [mybmw] add upgrade instructions Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/README.md | 5 +- .../main/resources/OH-INF/thing/thing-bev.xml | 4 + .../resources/OH-INF/thing/thing-bev_rex.xml | 4 + .../resources/OH-INF/thing/thing-conv.xml | 4 + .../resources/OH-INF/thing/thing-phev.xml | 4 + .../resources/OH-INF/update/thing-update.xml | 93 +++++++++++++++++++ 6 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/update/thing-update.xml diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md index beb186b2d88d8..085b970950a71 100644 --- a/bundles/org.openhab.binding.mybmw/README.md +++ b/bundles/org.openhab.binding.mybmw/README.md @@ -515,9 +515,8 @@ If these preconditions are fulfilled proceed with the fingerprint generation. Login to the openHAB console and use the `mybmw fingerprint` command. Fingerprint information on your account and vehicle(s) will show in the console and can be copiedfrom there. -A zip file with fingerprint information for your vehicle(s) will also be generated and put in the `mybmw` -folder in your home directory. -This fingerprint information is valuable for the developers to better support your vehicle in thesoftware. +A zip file with fingerprint information for your vehicle(s) will also be generated and put into the `mybmw` folder in the userdata folder. +This fingerprint information is valuable for the developers to better support your vehicle. You can restrict the accounts and vehicles for the fingerprint generation. Full syntax is available through the `mybmw help` console command. diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml index 593b9f516cc73..26a41c67630b1 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev.xml @@ -27,6 +27,10 @@ + + 4 + + vin diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml index 5e45f36d9712a..b16bc27e5ea05 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-bev_rex.xml @@ -27,6 +27,10 @@ + + 4 + + vin diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml index b95df69f01bb9..e694961a7600a 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-conv.xml @@ -24,6 +24,10 @@ + + 4 + + vin diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml index 35be350bfa522..3baa3faecb7bc 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/thing-phev.xml @@ -27,6 +27,10 @@ + + 4 + + vin diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/update/thing-update.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/update/thing-update.xml new file mode 100644 index 0000000000000..5146afe1904b5 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/update/thing-update.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + mybmw:charging-remaining-channel + + + + mybmw:last-fetched-channel + + + + mybmw:estimated-fuel-l-100km-channel + + + + mybmw:estimated-fuel-mpg-channel + + + + + + + + + + + + + mybmw:charging-remaining-channel + + + + mybmw:last-fetched-channel + + + + + + + + + + + mybmw:last-fetched-channel + + + + mybmw:estimated-fuel-l-100km-channel + + + + mybmw:estimated-fuel-mpg-channel + + + + + + + + + + + + + mybmw:charging-remaining-channel + + + + mybmw:last-fetched-channel + + + + mybmw:estimated-fuel-l-100km-channel + + + + mybmw:estimated-fuel-mpg-channel + + + + + + + From fef673bee0bab6f0702a80103d66d80178d8091d Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Sat, 21 Oct 2023 21:30:49 +0200 Subject: [PATCH 14/64] [mybmw] improve concurrency handling Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/pom.xml | 3 +++ .../mybmw/internal/handler/MyBMWBridgeHandler.java | 14 +++++++++++++- .../internal/handler/backend/MyBMWHttpProxy.java | 3 ++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/pom.xml b/bundles/org.openhab.binding.mybmw/pom.xml index 1e0a179e58457..cee4f94cb3c55 100644 --- a/bundles/org.openhab.binding.mybmw/pom.xml +++ b/bundles/org.openhab.binding.mybmw/pom.xml @@ -25,6 +25,9 @@ + + javax.measure.*;version="[2.1,3)" + diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java index bf5ec4bb80651..5f415989bc6a0 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java @@ -95,12 +95,23 @@ public void initialize() { // test-jar profile String environment = System.getenv(ENVIRONMENT); + if (environment == null) { + environment = ""; + } + + createMyBmwProxy(config, environment); + initializerJob = Optional.of(scheduler.schedule(this::discoverVehicles, 2, TimeUnit.SECONDS)); + } + } + + private synchronized void createMyBmwProxy(MyBMWBridgeConfiguration config, String environment) { + if (!myBmwProxy.isPresent()) { if (!(TEST.equals(environment) && TESTUSER.equals(config.userName))) { myBmwProxy = Optional.of(new MyBMWHttpProxy(httpClientFactory, config)); } else { myBmwProxy = Optional.of(new MyBMWFileProxy(httpClientFactory, config)); } - initializerJob = Optional.of(scheduler.schedule(this::discoverVehicles, 2, TimeUnit.SECONDS)); + logger.trace("xxxMyBMWBridgeHandler proxy set"); } } @@ -138,6 +149,7 @@ public Collection> getServices() { public Optional getMyBmwProxy() { logger.trace("xxxMyBMWBridgeHandler.getProxy"); + createMyBmwProxy(getConfigAs(MyBMWBridgeConfiguration.class), ENVIRONMENT); return myBmwProxy; } } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java index 28f24ba102539..290be8abc7ec8 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -79,7 +79,7 @@ public class MyBMWHttpProxy implements MyBMWProxy { private final String remoteStatusUrl; public MyBMWHttpProxy(HttpClientFactory httpClientFactory, MyBMWBridgeConfiguration bridgeConfiguration) { - logger.trace("MyBMWHttpProxy - initialize"); + logger.trace("xxxMyBMWHttpProxy - initialize"); httpClient = httpClientFactory.getCommonHttpClient(); myBMWTokenHandler = new MyBMWTokenController(bridgeConfiguration, httpClient); @@ -94,6 +94,7 @@ public MyBMWHttpProxy(HttpClientFactory httpClientFactory, MyBMWBridgeConfigurat remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) + BimmerConstants.API_REMOTE_SERVICE_BASE_URL; remoteStatusUrl = remoteCommandUrl + "eventStatus"; + logger.trace("xxxMyBMWHttpProxy - ready"); } @Override From 0b17819bec6a14c5f256217ea553f08791dc7db8 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Sat, 21 Oct 2023 21:39:29 +0200 Subject: [PATCH 15/64] [mybmw] deleted .gitignore Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/.gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 bundles/org.openhab.binding.mybmw/.gitignore diff --git a/bundles/org.openhab.binding.mybmw/.gitignore b/bundles/org.openhab.binding.mybmw/.gitignore deleted file mode 100644 index b507aafbd8664..0000000000000 --- a/bundles/org.openhab.binding.mybmw/.gitignore +++ /dev/null @@ -1 +0,0 @@ -jacoco.exec From 550c3aeea940da13c0373cc17726b84bf7109a4e Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Sat, 21 Oct 2023 21:51:35 +0200 Subject: [PATCH 16/64] [mybmw] remove ampersand from javadoc Signed-off-by: Martin Grassl --- .../java/org/openhab/binding/mybmw/internal/MyBMWConstants.java | 2 +- .../binding/mybmw/internal/dto/charge/ChargingProfile.java | 2 +- .../org/openhab/binding/mybmw/internal/dto/charge/Time.java | 2 +- .../binding/mybmw/internal/handler/RemoteServiceExecutor.java | 2 +- .../openhab/binding/mybmw/internal/handler/VehicleHandler.java | 2 +- .../binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java index b930e4b6f1991..0569b92698a94 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java @@ -22,7 +22,7 @@ * used across the whole binding. * * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - edit & send of charge profile + * @author Norbert Truchsess - edit and send of charge profile * @author Martin Grassl - updated enum values */ @NonNullByDefault diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java index ab80e7d6703e0..8a05f2e880140 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingProfile.java @@ -19,7 +19,7 @@ * The {@link ChargingProfile} Data Transfer Object * * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - edit & send of charge profile + * @author Norbert Truchsess - edit and send of charge profile * @author Martin Grassl - refactored to Java Bean */ public class ChargingProfile { diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java index 64ff1e3f4643e..40c48be392d1b 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/charge/Time.java @@ -16,7 +16,7 @@ * The {@link Time} Data Transfer Object * * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - edit & send of charge profile + * @author Norbert Truchsess - edit and send of charge profile * @author Martin Grassl - refactored to Java Bean */ public class Time { diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java index 3025a281ac3ee..9c6f478156883 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/RemoteServiceExecutor.java @@ -34,7 +34,7 @@ * @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 + * @author Norbert Truchsess - edit and send of charge profile * @author Martin Grassl - rename and refactor for v2 */ @NonNullByDefault diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java index 3e6a9b6c65146..a41dc3c184045 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java @@ -170,7 +170,7 @@ * updated * * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - edit & send charge profile + * @author Norbert Truchsess - edit and send charge profile * @author Martin Grassl - refactoring, merge with VehicleChannelHandler * @author Mark Herwege - refactoring, V2 API charging */ diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java index 290be8abc7ec8..7cc9ef1d014b2 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -58,7 +58,7 @@ * https://customer.bmwgroup.com/one/app/oauth.js * * @author Bernd Weymann - Initial contribution - * @author Norbert Truchsess - edit & send of charge profile + * @author Norbert Truchsess - edit and send of charge profile * @author Martin Grassl - refactoring * @author Mark Herwege - extended log anonymization */ From ec356fc18540347b06da08ae4c69ee1cd3313ee4 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Sun, 29 Oct 2023 21:07:21 +0100 Subject: [PATCH 17/64] [mybmw] changed null check Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/utils/MyBMWConfigurationChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java index ff559bd2eab33..d4b9d8b29b0cf 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/MyBMWConfigurationChecker.java @@ -25,7 +25,7 @@ @NonNullByDefault public final class MyBMWConfigurationChecker { public static boolean checkConfiguration(MyBMWBridgeConfiguration config) { - if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) { + if (config.userName.isBlank() || config.password.isBlank()) { return false; } else { return BimmerConstants.EADRAX_SERVER_MAP.containsKey(config.region); From 40a34b4b019139ee9672264addc764192c9fc0e7 Mon Sep 17 00:00:00 2001 From: mgrassl Date: Mon, 30 Oct 2023 20:16:24 +0100 Subject: [PATCH 18/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java Co-authored-by: Jacob Laursen Signed-off-by: mgrassl --- .../org/openhab/binding/mybmw/internal/utils/HTTPConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java index 68226eae3d729..294450a21bb75 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/HTTPConstants.java @@ -15,7 +15,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link HTTPConstants} class contains fields mapping thing configuration parameters. + * The {@link HTTPConstants} interface contains fields mapping thing configuration parameters. * * @author Bernd Weymann - Initial contribution * @author Martin Grassl - added image content type From e39476dedb3187bf6c8dea0a0ef83d7ffa183cc2 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 30 Oct 2023 20:25:46 +0100 Subject: [PATCH 19/64] [mybmw] remove unnecessary pom entry Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/pom.xml b/bundles/org.openhab.binding.mybmw/pom.xml index cee4f94cb3c55..079ee4d35c3d7 100644 --- a/bundles/org.openhab.binding.mybmw/pom.xml +++ b/bundles/org.openhab.binding.mybmw/pom.xml @@ -25,10 +25,6 @@ - - javax.measure.*;version="[2.1,3)" - - test-coverage From 5f809f550331715fda013e949ac998de9c76b274 Mon Sep 17 00:00:00 2001 From: mgrassl Date: Mon, 30 Oct 2023 20:47:14 +0100 Subject: [PATCH 20/64] Update bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java Co-authored-by: Jacob Laursen Signed-off-by: mgrassl --- .../binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java index c5374e4f1cbab..e7d08eba6a3aa 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleBaseTest.java @@ -33,7 +33,7 @@ * * checks the vehicleBase response * - * @author Martin Grassl - initial contribution + * @author Martin Grassl - Initial contribution */ public class VehicleBaseTest { From 72eb84bfb0d283ae9e376535a56500a06ed337d6 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 30 Oct 2023 20:57:13 +0100 Subject: [PATCH 21/64] [mybmw] remove commented out code Signed-off-by: Martin Grassl --- .../mybmw/internal/handler/auth/AuthTest.java | 63 +++---------------- 1 file changed, 9 insertions(+), 54 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java index badc7ef167691..b341f1acafb52 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/auth/AuthTest.java @@ -139,10 +139,7 @@ public void testAuth() { logger.info("Auth"); logger.info(aqr.tokenEndpoint); - // AuthenticationStore authenticationStore = authHttpClient.getAuthenticationStore(); - // BasicAuthentication ba = new BasicAuthentication(new URI(aqr.tokenEndpoint), Authentication.ANY_REALM, - // aqr.clientId, aqr.clientSecret); - // authenticationStore.addAuthentication(ba); + Request codeRequest = authHttpClient.POST(aqr.tokenEndpoint); String basicAuth = "Basic " + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes()); @@ -177,30 +174,19 @@ public void testAuth() { vehicleParams.put("tireGuardMode", "ENABLED"); vehicleParams.put("appDateTime", Long.toString(System.currentTimeMillis())); vehicleParams.put("apptimezone", "60"); - // vehicleRequest.param("tireGuardMode", "ENABLED"); - // vehicleRequest.param("appDateTime", Long.toString(System.currentTimeMillis())); - // vehicleRequest.param("apptimezone", "60.0"); - // vehicleRequest. - // // logger.info(vehicleParams); - // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, vehicleParams.toString(), - // StandardCharsets.UTF_8)); - // logger.info(vehicleRequest.getHeaders()); + String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false); String vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW) + "/eadrax-vcs/v1/vehicles"; logger.info(vehicleUrl); - Request vehicleRequest = apiHttpClient.newRequest(vehicleUrl + "?" + params);// - // .param("tireGuardMode", "ENABLED") - // .param("appDateTime", Long.toString(System.currentTimeMillis())).param("apptimezone", "60.0"); - // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded"); + Request vehicleRequest = apiHttpClient.newRequest(vehicleUrl + "?" + params); + vehicleRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken()); vehicleRequest.header("accept", "application/json"); vehicleRequest.header("accept-language", "de"); vehicleRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)"); - // vehicleRequest.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded"); - // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, - // UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); + ContentResponse vehicleResponse = vehicleRequest.send(); logger.info("Vehicle Status {} {}", vehicleResponse.getStatus(), vehicleResponse.getContentAsString()); @@ -217,17 +203,12 @@ public void testAuth() { Request chargeStatisticsRequest = apiHttpClient.newRequest(chargeStatisticsUrl) .param("vin", "WBY1Z81040V905639").param("currentDate", Converter.getCurrentISOTime()); logger.info("{}", chargeStatisticsUrl); - // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded"); + chargeStatisticsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken()); chargeStatisticsRequest.header("accept", "application/json"); chargeStatisticsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)"); chargeStatisticsRequest.header("accept-language", "de"); - // MultiMap chargeStatisticsParams = new MultiMap(); - // chargeStatisticsParams.put("vin", "WBY1Z81040V905639"); - // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime()); - // - // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false); logger.info("{}", params); chargeStatisticsRequest .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8)); @@ -251,20 +232,13 @@ public void testAuth() { + "/eadrax-chs/v1/charging-sessions"; Request chargeSessionsRequest = apiHttpClient.newRequest(chargeSessionsUrl + "?" + params); logger.info("{}", chargeSessionsUrl); - // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded"); + chargeSessionsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken()); chargeSessionsRequest.header("accept", "application/json"); chargeSessionsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)"); chargeSessionsRequest.header("accept-language", "de"); - // MultiMap chargeStatisticsParams = new MultiMap(); - // chargeStatisticsParams.put("vin", "WBY1Z81040V905639"); - // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime()); - // - // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false); logger.info("{}", params); - // chargeStatisticsRequest - // .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8)); ContentResponse chargeSessionsResponse = chargeSessionsRequest.send(); logger.info("{}", chargeSessionsResponse.getStatus()); @@ -275,32 +249,13 @@ public void testAuth() { + "/eadrax-vrccs/v2/presentation/remote-commands/WBY1Z81040V905639/charging-control"; Request chargingControlRequest = apiHttpClient.POST(chargingControlUrl); logger.info("{}", chargingControlUrl); - // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded"); + chargingControlRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken()); chargingControlRequest.header("accept", "application/json"); chargingControlRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)"); chargingControlRequest.header("accept-language", "de"); chargingControlRequest.header("Content-Type", CONTENT_TYPE_JSON); - // String content = FileReader.readFileInString("responses/charging-profile.json"); - // logger.info("{}", content); - // ChargeProfile cpc = JsonStringDeserializer.deserializeString(content, ChargeProfile.class); - // String contentTranfsorm = Converter.getGson().toJson(cpc); - // String profile = "{chargingProfile:" + contentTranfsorm + "}"; - // logger.info("{}", profile); - // chargingControlRequest - // .content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8)); - - // chargeStatisticsParams.put("vin", "WBY1Z81040V905639"); - // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime()); - // - // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false); - - // ContentResponse chargingControlResponse = chargingControlRequest.send(); - // logger.info("{}", chargingControlResponse.getStatus()); - // logger.info("{}", chargingControlResponse.getReason()); - // logger.info("{}", chargingControlResponse.getContentAsString()); - } catch (Exception e) { logger.error("{}", e.getMessage()); } @@ -417,7 +372,7 @@ public void testChinaToken() { // https://stackoverflow.com/questions/11410770/load-rsa-public-key-from-file String publicKeyStr = pkr.data.value; - // String cleanPublicKeyStr = pkr.data.value.replaceAll("(\r\n|\n)", Constants.EMPTY); + String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "") .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", ""); byte[] encoded = Base64.getDecoder().decode(publicKeyPEM); From 52c16b1499d11fba3ed8de5b1f23cc50895c6082 Mon Sep 17 00:00:00 2001 From: mgrassl Date: Mon, 30 Oct 2023 21:00:52 +0100 Subject: [PATCH 22/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java Co-authored-by: Jacob Laursen Signed-off-by: mgrassl --- .../mybmw/internal/handler/auth/MyBMWTokenController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java index 3b116afbdff5e..f6c3996a58041 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java @@ -121,8 +121,6 @@ public Token getToken() { tokenUpdateSuccess = updateTokenChina(); break; case REGION_NORTH_AMERICA: - tokenUpdateSuccess = updateToken(); - break; case REGION_ROW: tokenUpdateSuccess = updateToken(); break; From 7f7ceecca8693604f96563f2cd84ca5f8592c9f2 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 30 Oct 2023 21:11:54 +0100 Subject: [PATCH 23/64] [mybmw] deleted i18n files, moving to crowdin Signed-off-by: Martin Grassl --- .../resources/OH-INF/i18n/mybmw_de.properties | 245 ----------------- .../resources/OH-INF/i18n/mybmw_fi.properties | 257 ------------------ 2 files changed, 502 deletions(-) delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties delete mode 100644 bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_fi.properties diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties deleted file mode 100644 index a76faae3d6f5c..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties +++ /dev/null @@ -1,245 +0,0 @@ -# add-on - -addon.mybmw.name = MyBMW -addon.mybmw.description = Fahrzeugdaten über die MyBMW App - -# thing types - -thing-type.mybmw.account.label = MyBMW Benutzerkonto -thing-type.mybmw.account.description = Kontodaten für das BMW Benutzerkonto -thing-type.mybmw.bev.label = Elektrofahrzeug -thing-type.mybmw.bev.description = Batterieelektrisches Fahrzeug (bev) -thing-type.mybmw.bev_rex.label = Elektrofahrzeug mit REX -thing-type.mybmw.bev_rex.description = Elektrofahrzeug mit Range Extender (bev_rex) -thing-type.mybmw.conv.label = Konventionelles Fahrzeug -thing-type.mybmw.conv.description = Konventionelles Benzin/Diesel Fahrzeug (conv) -thing-type.mybmw.phev.label = Plug-in-Hybrid Elektrofahrzeug -thing-type.mybmw.phev.description = Konventionelles Fahrzeug mit Elektromotor (phev) - -# thing types config - -thing-type.config.mybmw.bridge.language.label = Sprachauswahl -thing-type.config.mybmw.bridge.language.description = Daten werden für die gewünschte Sprache angefordert (en, de, fr ...) -thing-type.config.mybmw.bridge.password.label = Passwort -thing-type.config.mybmw.bridge.password.description = Passwort für die MyBMW App -thing-type.config.mybmw.bridge.region.label = Region -thing-type.config.mybmw.bridge.region.description = Auswahl Ihrer Region -thing-type.config.mybmw.bridge.region.label = Region -thing-type.config.mybmw.bridge.region.option.CHINA = China -thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = Nordamerika -thing-type.config.mybmw.bridge.region.option.ROW = Rest der Welt -thing-type.config.mybmw.bridge.userName.label = Benutzername -thing-type.config.mybmw.bridge.userName.description = Benutzername für die MyBMW App -thing-type.config.mybmw.vehicle.refreshInterval.label = Datenaktualisierung in Minuten -thing-type.config.mybmw.vehicle.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs -thing-type.config.mybmw.vehicle.vehicleBrand.label = Marke des Fahrzeugs -thing-type.config.mybmw.vehicle.vehicleBrand.description = Fahrzeugmarke wie z.B. BMW oder Mini. -thing-type.config.mybmw.vehicle.vin.label = Fahrzeug Identifikationsnummer (VIN) -thing-type.config.mybmw.vehicle.vin.description = VIN des Fahrzeugs - -# channel group types - -channel-group-type.mybmw.charge-statistic.label = Elektrische Ladestatistik -channel-group-type.mybmw.charge-statistic.description = Statistik der Ladevorgänge im Monat -channel-group-type.mybmw.check-control-values.label = Warnungen -channel-group-type.mybmw.check-control-values.description = Aktuelle Warnungen des Fahrzeugs -channel-group-type.mybmw.conv-range-values.label = Verbrenner Reichweiten und Füllstände -channel-group-type.mybmw.conv-range-values.description = Tachostand, Reichweite und Tankfüllung des Fahrzeugs -channel-group-type.mybmw.door-values.label = Details aller Türen -channel-group-type.mybmw.door-values.description = Zeigt die Details der Türen und Fenster des Fahrzeugs -channel-group-type.mybmw.ev-range-values.label = Elektro- Reichweiten und Batterieladung -channel-group-type.mybmw.ev-range-values.description = Tachostand, Reichweiten und Ladestand des Fahrzeugs -channel-group-type.mybmw.ev-vehicle-status.label = Fahrzeug Zustand -channel-group-type.mybmw.ev-vehicle-status.description = Gesamtzustand des Fahrzeugs -channel-group-type.mybmw.hybrid-range-values.label = Hybride Reichweiten und Füllstände -channel-group-type.mybmw.hybrid-range-values.description = Tachostand, Reichweite, Ladezustand und Tankfüllung für hybride Fahrzeuge -channel-group-type.mybmw.image-values.label = Fahrzeug Bild -channel-group-type.mybmw.image-values.description = Bild des Fahrzeug basierend auf der ausgewählten Ansicht -channel-group-type.mybmw.location-values.label = Fahrzeug Standort -channel-group-type.mybmw.location-values.description = Koordinaten und Ausrichtung des Fahrzeugs -channel-group-type.mybmw.profile-values.label = Elektrisches Ladeprofil -channel-group-type.mybmw.profile-values.description = Zeitplanung der Ladevorgänge -channel-group-type.mybmw.remote-services.label = Fernsteuerung -channel-group-type.mybmw.remote-services.description = Fernsteuerung des Fahrzeugs -channel-group-type.mybmw.service-values.label = Wartung -channel-group-type.mybmw.service-values.description = Anstehende Wartungstermine des Fahrzeugs -channel-group-type.mybmw.session-values.label = Elektrische Ladevorgänge -channel-group-type.mybmw.session-values.description = Liste der letzten Ladevorgänge -channel-group-type.mybmw.tire-pressures.label = Reifen Luftdruck -channel-group-type.mybmw.tire-pressures.description = Reifen Luftdruck Ist und Sollwerte -channel-group-type.mybmw.vehicle-status.label = Fahrzeug Zustand -channel-group-type.mybmw.vehicle-status.description = Gesamtzustand des Fahrzeugs - -# channel types - -channel-type.mybmw.address-channel.label = Adresse -channel-type.mybmw.charging-info-channel.label = Ladeinformationen -channel-type.mybmw.charging-status-channel.label = Ladezustand -channel-type.mybmw.check-control-channel.label = Warnung Aktiv -channel-type.mybmw.checkcontrol-details-channel.label = Warnung Details -channel-type.mybmw.checkcontrol-name-channel.label = Warnung -channel-type.mybmw.checkcontrol-severity-channel.label = Warnung Priorität -channel-type.mybmw.doors-channel.label = Gesamtzustand der Türen -channel-type.mybmw.driver-front-channel.label = Fahrertür -channel-type.mybmw.driver-rear-channel.label = Fahrertür Hinten -channel-type.mybmw.front-left-current-channel.label = Reifen Luftdruck Vorne Links -channel-type.mybmw.front-left-target-channel.label = Reifen Luftdruck vorne links -channel-type.mybmw.front-right-current-channel.label = Reifen Luftdruck Vorne Rechts -channel-type.mybmw.front-right-target-channel.label = Reifen Luftdruck vorne rechts -channel-type.mybmw.gps-channel.label = Koordinaten -channel-type.mybmw.heading-channel.label = Ausrichtung -channel-type.mybmw.home-distance-channel.label = Entfernung von Zuhause -channel-type.mybmw.home-distance-channel.description = Berechnete Entfernung zwischen Fahrzeug und Heimatort -channel-type.mybmw.hood-channel.label = Frontklappe -channel-type.mybmw.image-view-channel.label = Fahrzeug Ansicht -channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Front Seitenansicht -channel-type.mybmw.image-view-channel.command.option.VehicleInfo = Frontansicht -channel-type.mybmw.image-view-channel.command.option.ChargingHistory = Seitenansicht -channel-type.mybmw.image-view-channel.command.option.Default = Standard Ansicht -channel-type.mybmw.last-update-channel.label = Letzte Aktualisierung -channel-type.mybmw.last-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH\:%1$tM -channel-type.mybmw.lock-channel.label = Fahrzeug Abgeschlossen -channel-type.mybmw.mileage-channel.label = Tachostand -channel-type.mybmw.motion-channel.label = Fahrzustand -channel-type.mybmw.next-service-date-channel.label = Nächster Service Termin -channel-type.mybmw.next-service-date-channel.state.pattern = %1$tb %1$tY -channel-type.mybmw.next-service-mileage-channel.label = Nächster Service in Kilometern -channel-type.mybmw.passenger-front-channel.label = Beifahrertür -channel-type.mybmw.passenger-rear-channel.label = Beifahrertür Hinten -channel-type.mybmw.plug-connection-channel.label = Ladestecker -channel-type.mybmw.png-channel.label = Fahrzeug Bild -channel-type.mybmw.profile-climate-channel.label = Klimatisierung bei Abfahrt -channel-type.mybmw.profile-control-channel.label = Ladeplan -channel-type.mybmw.profile-control-channel.description = Ladeplan Auswahl -channel-type.mybmw.profile-control-channel.command.option.weeklyPlanner = Wochenplan -channel-type.mybmw.profile-limit-channel.label = Ladung Limitiert -channel-type.mybmw.profile-limit-channel.description = Limitiertes Laden aktiviert -channel-type.mybmw.profile-mode-channel.label = Ladeprofil -channel-type.mybmw.profile-mode-channel.description = Modus für sofortiges oder verzögertes Laden -channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Sofort Laden -channel-type.mybmw.profile-mode-channel.command.option.delayedCharging = Ladeverzögerung -channel-type.mybmw.profile-prefs-channel.label = Ladeprofil Präferenz -channel-type.mybmw.profile-prefs-channel.description = Einstellungen für verzögerte Ladung -channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Keine Präferenz -channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Laden im Zeitfenster -channel-type.mybmw.profile-target-channel.label = Ziel Ladezustand -channel-type.mybmw.profile-target-channel.description = Erwünschter Batterie Ladezustand -channel-type.mybmw.range-electric-channel.label = Elektrische Reichweite -channel-type.mybmw.range-fuel-channel.label = Verbrenner Reichweite -channel-type.mybmw.range-hybrid-channel.label = Hybride Reichweite -channel-type.mybmw.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius -channel-type.mybmw.range-radius-fuel-channel.label = Verbrenner Reichweiten-Radius -channel-type.mybmw.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius -channel-type.mybmw.raw-channel.label = Rohdaten -channel-type.mybmw.rear-left-current-channel.label = Reifen Luftdruck Hinten Links -channel-type.mybmw.rear-left-target-channel.label = Reifen Luftdruck hinten links -channel-type.mybmw.rear-right-current-channel.label = Reifen Luftdruck Hinten Rechts -channel-type.mybmw.rear-right-target-channel.label = Reifen Luftdruck hinten rechts -channel-type.mybmw.remaining-fuel-channel.label = Tankstand -channel-type.mybmw.remote-command-channel.label = Kommando Auswahl -channel-type.mybmw.remote-state-channel.label = Ausführungszustand -channel-type.mybmw.service-date-channel.label = Service Termin -channel-type.mybmw.service-date-channel.state.pattern = %1$tb %1$tY -channel-type.mybmw.service-details-channel.label = Service Details -channel-type.mybmw.service-mileage-channel.label = Service in Kilometern -channel-type.mybmw.service-name-channel.label = Service -channel-type.mybmw.session-energy-channel.label = Energie Geladen -channel-type.mybmw.session-issue-channel.label = Ladevorgang Probleme -channel-type.mybmw.session-status-channel.label = Ladevorgang Zustand -channel-type.mybmw.session-subtitle-channel.label = Ladevorgang Details -channel-type.mybmw.session-title-channel.label = Ladevorgang Beschreibung -channel-type.mybmw.soc-channel.label = Batterie Ladestand -channel-type.mybmw.statistic-energy-channel.label = Energie Geladen Monat -channel-type.mybmw.statistic-energy-channel.description = Geladene Energie in diesem Monat -channel-type.mybmw.statistic-sessions-channel.label = Ladevorgänge Monat -channel-type.mybmw.statistic-sessions-channel.description = Anzahl der Ladevorgänge in diesem Monat -channel-type.mybmw.statistic-title-channel.label = Ladestatistik Monat -channel-type.mybmw.sunroof-channel.label = Schiebedach -channel-type.mybmw.timer1-day-fri-channel.label = Zeitprofil 1 - Freitag -channel-type.mybmw.timer1-day-fri-channel.description = Freitags Planung für Timer 1 -channel-type.mybmw.timer1-day-mon-channel.label = Zeitprofil 1 - Montag -channel-type.mybmw.timer1-day-mon-channel.description = Montags Planung für Timer 1 -channel-type.mybmw.timer1-day-sat-channel.label = Zeitprofil 1 - Samstag -channel-type.mybmw.timer1-day-sat-channel.description = Samstags Planung für Timer 1 -channel-type.mybmw.timer1-day-sun-channel.label = Zeitprofil 1 - Sonntag -channel-type.mybmw.timer1-day-sun-channel.description = Sonntags Planung für Timer 1 -channel-type.mybmw.timer1-day-thu-channel.label = Zeitprofil 1 - Donnerstag -channel-type.mybmw.timer1-day-thu-channel.description = Donnerstags Planung für Timer 1 -channel-type.mybmw.timer1-day-tue-channel.label = Zeitprofil 1 - Dienstag -channel-type.mybmw.timer1-day-tue-channel.description = Dienstags Planung für Timer 1 -channel-type.mybmw.timer1-day-wed-channel.label = Zeitprofil 1 - Mittwoch -channel-type.mybmw.timer1-day-wed-channel.description = Mittwochs Planung für Timer 1 -channel-type.mybmw.timer1-departure-channel.label = Zeitprofil 1 - Abfahrtszeit -channel-type.mybmw.timer1-departure-channel.description = Abfahrtszeit für regelmäßige Planung Timer 1 -channel-type.mybmw.timer1-departure-channel.state.pattern = %1$tH\:%1$tM -channel-type.mybmw.timer1-enabled-channel.label = Zeitprofil 1 - Aktiviert -channel-type.mybmw.timer1-enabled-channel.description = Timer 1 aktiviert -channel-type.mybmw.timer2-day-fri-channel.label = Zeitprofil 2 - Freitag -channel-type.mybmw.timer2-day-fri-channel.description = Freitags Planung für Timer 2 -channel-type.mybmw.timer2-day-mon-channel.label = Zeitprofil 2 - Montag -channel-type.mybmw.timer2-day-mon-channel.description = Montags Planung für Timer 2 -channel-type.mybmw.timer2-day-sat-channel.label = Zeitprofil 2 - Samstag -channel-type.mybmw.timer2-day-sat-channel.description = Samstags Planung für Timer 2 -channel-type.mybmw.timer2-day-sun-channel.label = Zeitprofil 2 - Sonntag -channel-type.mybmw.timer2-day-sun-channel.description = Sonntags Planung für Timer 2 -channel-type.mybmw.timer2-day-thu-channel.label = Zeitprofil 2 - Donnerstag -channel-type.mybmw.timer2-day-thu-channel.description = Donnerstags Planung für Timer 2 -channel-type.mybmw.timer2-day-tue-channel.label = Zeitprofil 2 - Dienstag -channel-type.mybmw.timer2-day-tue-channel.description = Dienstags Planung für Timer 2 -channel-type.mybmw.timer2-day-wed-channel.label = Zeitprofil 2 - Mittwoch -channel-type.mybmw.timer2-day-wed-channel.description = Mittwochs Planung für Timer 2 -channel-type.mybmw.timer2-departure-channel.label = Zeitprofil 2 - Abfahrtszeit -channel-type.mybmw.timer2-departure-channel.description = Abfahrtszeit für regelmäßige Planung Timer 2 -channel-type.mybmw.timer2-departure-channel.state.pattern = %1$tH\:%1$tM -channel-type.mybmw.timer2-enabled-channel.label = Zeitprofil 2 - Aktiviert -channel-type.mybmw.timer2-enabled-channel.description = Timer 2 aktiviert -channel-type.mybmw.timer3-day-fri-channel.label = Zeitprofil 3 - Freitag -channel-type.mybmw.timer3-day-fri-channel.description = Freitags Planung für Timer 3 -channel-type.mybmw.timer3-day-mon-channel.label = Zeitprofil 3 - Montag -channel-type.mybmw.timer3-day-mon-channel.description = Montags Planung für Timer 3 -channel-type.mybmw.timer3-day-sat-channel.label = Zeitprofil 3 - Samstag -channel-type.mybmw.timer3-day-sat-channel.description = Samstags Planung für Timer 3 -channel-type.mybmw.timer3-day-sun-channel.label = Zeitprofil 3 - Sonntag -channel-type.mybmw.timer3-day-sun-channel.description = Sonntags Planung für Timer 3 -channel-type.mybmw.timer3-day-thu-channel.label = Zeitprofil 3 - Donnerstag -channel-type.mybmw.timer3-day-thu-channel.description = Donnerstags Planung für Timer 3 -channel-type.mybmw.timer3-day-tue-channel.label = Zeitprofil 3 - Dienstag -channel-type.mybmw.timer3-day-tue-channel.description = Dienstags Planung für Timer 3 -channel-type.mybmw.timer3-day-wed-channel.label = Zeitprofil 3 - Mittwoch -channel-type.mybmw.timer3-day-wed-channel.description = Mittwochs Planung für Timer 3 -channel-type.mybmw.timer3-departure-channel.label = Zeitprofil 3 - Abfahrtszeit -channel-type.mybmw.timer3-departure-channel.description = Abfahrtszeit für regelmäßige Planung Timer 3 -channel-type.mybmw.timer3-departure-channel.state.pattern = %1$tH\:%1$tM -channel-type.mybmw.timer3-enabled-channel.label = Zeitprofil 3 - Aktiviert -channel-type.mybmw.timer3-enabled-channel.description = Timer 3 aktiviert -channel-type.mybmw.timer4-day-fri-channel.label = Zeitprofil 4 - Freitag -channel-type.mybmw.timer4-day-fri-channel.description = Freitags Planung für Timer 4 -channel-type.mybmw.timer4-day-mon-channel.label = Zeitprofil 4 - Montag -channel-type.mybmw.timer4-day-mon-channel.description = Montags Planung für Timer 4 -channel-type.mybmw.timer4-day-sat-channel.label = Zeitprofil 4 - Samstag -channel-type.mybmw.timer4-day-sat-channel.description = Samstags Planung für Timer 4 -channel-type.mybmw.timer4-day-sun-channel.label = Zeitprofil 4 - Sonntag -channel-type.mybmw.timer4-day-sun-channel.description = Sonntags Planung für Timer 4 -channel-type.mybmw.timer4-day-thu-channel.label = Zeitprofil 4 - Donnerstag -channel-type.mybmw.timer4-day-thu-channel.description = Donnerstags Planung für Timer 4 -channel-type.mybmw.timer4-day-tue-channel.label = Zeitprofil 4 - Dienstag -channel-type.mybmw.timer4-day-tue-channel.description = Dienstags Planung für Timer 4 -channel-type.mybmw.timer4-day-wed-channel.label = Zeitprofil 4 - Mittwoch -channel-type.mybmw.timer4-day-wed-channel.description = Mittwochs Planung für Timer 4 -channel-type.mybmw.timer4-departure-channel.label = Zeitprofil 4 - Abfahrtszeit -channel-type.mybmw.timer4-departure-channel.description = Abfahrtszeit für regelmäßige Planung Timer 4 -channel-type.mybmw.timer4-departure-channel.state.pattern = %1$tH\:%1$tM -channel-type.mybmw.timer4-enabled-channel.label = Zeitprofil 4 - Aktiviert -channel-type.mybmw.timer4-enabled-channel.description = Timer 4 aktiviert -channel-type.mybmw.trunk-channel.label = Heckklappe -channel-type.mybmw.window-driver-front-channel.label = Fahrertür Fenster -channel-type.mybmw.window-driver-rear-channel.label = Fahrertür Hinten Fenster -channel-type.mybmw.window-end-channel.label = Ladefenster Endzeit -channel-type.mybmw.window-end-channel.description = Endzeit des Ladefensters -channel-type.mybmw.window-end-channel.state.pattern = %1$tH\:%1$tM -channel-type.mybmw.window-passenger-front-channel.label = Beifahrertür Fenster -channel-type.mybmw.window-passenger-rear-channel.label = Beifahrertür Hinten Fenster -channel-type.mybmw.window-start-channel.label = Ladefenster Startzeit -channel-type.mybmw.window-start-channel.description = Startzeit des Ladefensters -channel-type.mybmw.window-start-channel.state.pattern = %1$tH\:%1$tM -channel-type.mybmw.windows-channel.label = Gesamtzustand der Fenster diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_fi.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_fi.properties deleted file mode 100644 index f9f309d7214f3..0000000000000 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_fi.properties +++ /dev/null @@ -1,257 +0,0 @@ -# Binding -binding.mybmw.name = MyBMW -binding.mybmw.description = Provides access to your Vehicle Data like MyBMW App - -# thing types -thing-type.config.mybmw.bridge.language.description = Channel data can be returned in the desired language like en, de, fr ... -thing-type.config.mybmw.bridge.language.label = Kieliasetukset -thing-type.config.mybmw.bridge.password.description = MyBMW Salasana -thing-type.config.mybmw.bridge.password.label = Salasana -thing-type.config.mybmw.bridge.region.description = Valitse alue muodostaaksesi yhteyden sopivaan BMW-palvelimeen -thing-type.config.mybmw.bridge.region.label = Alue -thing-type.config.mybmw.bridge.region.option.CHINA = Kiina -thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = Pohjois-Amerikka -thing-type.config.mybmw.bridge.region.option.ROW = Muu maailma -thing-type.config.mybmw.bridge.userName.description = MyBMW Käyttäjätunnus -thing-type.config.mybmw.bridge.userName.label = Käyttäjätunnus - -thing-type.config.mybmw.vehicle.refreshInterval.description = Ajoneuvotietojen päivitystaajuus -thing-type.config.mybmw.vehicle.refreshInterval.label = Päivitysväli -thing-type.config.mybmw.vehicle.vehicleBrand.description = Automerkki kuten BMW tai Mini -thing-type.config.mybmw.vehicle.vehicleBrand.label = Ajoneuvon merkki -thing-type.config.mybmw.vehicle.vin.description = BMW:n antama VIN -thing-type.config.mybmw.vehicle.vin.label = Ajoneuvon valmistenumero (VIN) -thing-type.mybmw.account.description = BMW-tilisi tiedot -thing-type.mybmw.account.label = MyBMW tili -thing-type.mybmw.bev_rex.description = Akkukäyttöinen sähköajoneuvo kantaman pidentäjällä (BEV_REX) -thing-type.mybmw.bev_rex.label = Sähköauto REX:llä -thing-type.mybmw.bev.description = Akkukäyttöinen sähköajoneuvo (BEV) -thing-type.mybmw.bev.label = Sähköajoneuvo -thing-type.mybmw.conv.description = Perinteisellä polttoaineella varustettu ajoneuvo (CONV) -thing-type.mybmw.conv.label = Perinteinen ajoneuvo -thing-type.mybmw.phev.description = Ladattava hybridi (PHEV) -thing-type.mybmw.phev.label = Ladattava hybridi sähköajoneuvo - -# channel group types -channel-group-type.mybmw.charge-statistic.description = Kuluvan kuukauden lataustilastot -channel-group-type.mybmw.charge-statistic.label = Lataustilastot -channel-group-type.mybmw.check-control-values.description = Näyttää nykyiset aktiiviset CheckControl-viestit -channel-group-type.mybmw.check-control-values.label = CheckControl-viestit -channel-group-type.mybmw.conv-range-values.description = Näyttää ajokilometrimäärän, jäljellä olevan toimintamatkan ja polttoainetason arvot -channel-group-type.mybmw.conv-range-values.label = Toimintamatka ja polttoainetiedot -channel-group-type.mybmw.door-values.description = Kaikkien ovien ja ikkunoiden yksityiskohtainen tila -channel-group-type.mybmw.door-values.label = Yksityiskohtainen oven tila -channel-group-type.mybmw.ev-range-values.description = Näyttää mittarilukeman, jäljellä oleva toimintamatkan ja lataustason arvot -channel-group-type.mybmw.ev-range-values.label = Kantama- ja lataustiedot -channel-group-type.mybmw.ev-vehicle-status.description = Ajoneuvon yleistila -channel-group-type.mybmw.ev-vehicle-status.label = Ajoneuvon tila -channel-group-type.mybmw.hybrid-range-values.description = Näyttää ajokilometrit, jäljellä olevan polttoaineen ja ajomatkan hybridiautoille -channel-group-type.mybmw.hybrid-range-values.label = Toimintamatka, lataus/polttoainetiedot -channel-group-type.mybmw.image-values.description = Näyttää ajoneuvosi kuvan -channel-group-type.mybmw.image-values.label = Ajoneuvon kuva - -channel-group-type.mybmw.location-values.description = Ajoneuvon koordinaatit ja suunta -channel-group-type.mybmw.location-values.label = Ajoneuvon sijainti -channel-group-type.mybmw.profile-values.description = Ajoitetut latausprofiilit -channel-group-type.mybmw.profile-values.label = Sähkölatausprofiili -channel-group-type.mybmw.remote-services.description = Ajoneuvon etäohjaus -channel-group-type.mybmw.remote-services.label = Etäpalvelut -channel-group-type.mybmw.service-values.description = Auton tulevat huoltoaikataulut -channel-group-type.mybmw.service-values.label = Ajoneuvopalvelut -channel-group-type.mybmw.session-values.description = Aiemmat lataustapahtumat -channel-group-type.mybmw.session-values.label = Lataustapahtumat -channel-group-type.mybmw.tire-pressures.description = Nykyiset ja halutut paineet kaikkiin renkaisiin -channel-group-type.mybmw.tire-pressures.label = Rengaspaine -channel-group-type.mybmw.vehicle-status.description = Ajoneuvon yleistila -channel-group-type.mybmw.vehicle-status.label = Ajoneuvon tila - -# channel types -channel-type.mybmw.address-channel.label = Osoite -channel-type.mybmw.charging-info-channel.label = Lataustiedot -channel-type.mybmw.charging-remaining-channel.label = Jäljellä oleva latausaika -channel-type.mybmw.charging-status-channel.label = Lataustila -channel-type.mybmw.check-control-channel.label = Check Control -channel-type.mybmw.checkcontrol-details-channel.label = Check Control tiedot -channel-type.mybmw.checkcontrol-name-channel.label = Check Control kuvaus -channel-type.mybmw.checkcontrol-severity-channel.label = Vakavuustaso - -channel-type.mybmw.doors-channel.label = Oven yleistila -channel-type.mybmw.driver-front-channel.label = Kuljettajan ovi -channel-type.mybmw.driver-rear-channel.label = Kuljettajan puoleinen takaovi -channel-type.mybmw.estimated-fuel-l-100km-channel.label = Arvioitu kulutus l/100km -channel-type.mybmw.estimated-fuel-mpg-channel.label = Arvioitu kulutus mpg -channel-type.mybmw.front-left-current-channel.label = Rengaspaine vasen etu -channel-type.mybmw.front-left-target-channel.label = Rengaspaine vasen etu tavoite -channel-type.mybmw.front-right-current-channel.label = Rengaspaine oikea etu -channel-type.mybmw.front-right-target-channel.label = Rengaspaine oikea etu tavoite -channel-type.mybmw.gps-channel.label = GPS koordinaatit -channel-type.mybmw.heading-channel.label = Suuntakulma -channel-type.mybmw.home-distance-channel.description = Laskettu etäisyys ajoneuvon ja kodin välillä -channel-type.mybmw.home-distance-channel.label = Etäisyys kotoa -channel-type.mybmw.hood-channel.label = Konepelti - -channel-type.mybmw.image-view-channel.command.option.FrontLeft = Vasen sivunäkymä -channel-type.mybmw.image-view-channel.command.option.FrontRight = Oikea sivunäkymä -channel-type.mybmw.image-view-channel.command.option.FrontView = Etunäkymä -channel-type.mybmw.image-view-channel.command.option.RearView = Takanäkymä -channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Näkymä edestä -channel-type.mybmw.image-view-channel.label = Kuvan katseluportti -channel-type.mybmw.last-fetched-channel.label = Viimeisin Openhab-päivityksen aikaleima -channel-type.mybmw.last-update-channel.label = Viimeisin auton tilan aikaleima -channel-type.mybmw.lock-channel.label = Ovet lukittu -channel-type.mybmw.mileage-channel.label = Ajettu kokonaismatka - -channel-type.mybmw.next-service-date-channel.label = Seuraava huoltopäivämäärä -channel-type.mybmw.next-service-date-channel.state.pattern = %1$tb %1$tY -channel-type.mybmw.next-service-mileage-channel.label = Kilometrit seuraavaan huoltoon -channel-type.mybmw.passenger-front-channel.label = Matkustajan ovi -channel-type.mybmw.passenger-rear-channel.label = Matkustajan puoleinen takaovi -channel-type.mybmw.plug-connection-channel.label = Pistokeyhteyden tila -channel-type.mybmw.png-channel.label = Renderoidun ajoneuvon kuva - -channel-type.mybmw.profile-climate-channel.label = Ilmastointi lähtöaikana -channel-type.mybmw.profile-control-channel.command.option.weeklyPlanner = Viikkoaikataulu -channel-type.mybmw.profile-control-channel.description = Lataussuunnitelman valinta -channel-type.mybmw.profile-control-channel.label = Lataussuunnitelma -channel-type.mybmw.profile-limit-channel.description = Rajoitettu lataus aktivoitu -channel-type.mybmw.profile-limit-channel.label = Latausenergia rajoitettu -channel-type.mybmw.profile-mode-channel.command.option.delayedCharging = Käytä latausasetusta -channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Välitön lataus -channel-type.mybmw.profile-mode-channel.description = Välittömän tai viivästetyn latauksen tilan valinta -channel-type.mybmw.profile-mode-channel.label = Lataustila -channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Latausikkuna -channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Ei valintaa -channel-type.mybmw.profile-prefs-channel.description = Viivästetyn latauksen asetukset -channel-type.mybmw.profile-prefs-channel.label = Latausasetukset -channel-type.mybmw.profile-target-channel.description = Lataustilan tavoite -channel-type.mybmw.profile-target-channel.label = Lataustilan tavoite - -channel-type.mybmw.range-electric-channel.label = Sähköinen toimintamatka -channel-type.mybmw.range-fuel-channel.label = Toimintamatka polttoaineella -channel-type.mybmw.range-hybrid-channel.label = Yhdistetty toimintamatka -channel-type.mybmw.range-radius-electric-channel.label = Sähköinen toimintasäde -channel-type.mybmw.range-radius-fuel-channel.label = Toimintasäde polttoaineella -channel-type.mybmw.range-radius-hybrid-channel.label = Yhdistetty toimintasäde -channel-type.mybmw.raw-channel.label = Raakadata - -channel-type.mybmw.rear-left-current-channel.label = Rengaspaine vasen taka -channel-type.mybmw.rear-left-target-channel.label = Rengaspaine vasen taka tavoite -channel-type.mybmw.rear-right-current-channel.label = Rengaspaine oikea taka -channel-type.mybmw.rear-right-target-channel.label = Rengaspaine oikea taka tavoite -channel-type.mybmw.remaining-fuel-channel.label = Jäljellä oleva polttoaine -channel-type.mybmw.remote-command-channel.command.option.climate-now-start = Aloita ilmastointi nyt -channel-type.mybmw.remote-command-channel.command.option.climate-now-stop = Lopeta ilmastointi nyt -channel-type.mybmw.remote-command-channel.command.option.door-lock = Lukitse ajoneuvo -channel-type.mybmw.remote-command-channel.command.option.door-unlock = Avaa ajoneuvon lukitus -channel-type.mybmw.remote-command-channel.command.option.horn-blow = Soita äänitorvea -channel-type.mybmw.remote-command-channel.command.option.light-flash = Väläytä valoja -channel-type.mybmw.remote-command-channel.command.option.vehicle-finder = Etsi ajoneuvo -channel-type.mybmw.remote-command-channel.label = Etäkomento -channel-type.mybmw.remote-state-channel.label = Etäkomenmon tila -channel-type.mybmw.service-date-channel.label = Huollon päiväys -channel-type.mybmw.service-date-channel.state.pattern = %1$tb %1$tY -channel-type.mybmw.service-details-channel.label = Huollon yksityiskohdat -channel-type.mybmw.service-mileage-channel.label = Kilometrit huoltoon asti -channel-type.mybmw.service-name-channel.label = Palvelun nimi -channel-type.mybmw.session-energy-channel.label = Ladattu energia -channel-type.mybmw.session-issue-channel.label = Ongelmia latauksen aikana -channel-type.mybmw.session-status-channel.label = Latauksen tila -channel-type.mybmw.session-subtitle-channel.label = Latauksen yksityiskohdat -channel-type.mybmw.session-title-channel.label = Latauksen nimi -channel-type.mybmw.soc-channel.label = Akun lataustaso - -channel-type.mybmw.statistic-energy-channel.description = Kuluvan kuukauden ladattu kokonaisenergia -channel-type.mybmw.statistic-energy-channel.label = Latauskustannus -channel-type.mybmw.statistic-sessions-channel.description = Latauskertojen määrä tässä kuussa -channel-type.mybmw.statistic-sessions-channel.label = Latauskerrat -channel-type.mybmw.statistic-title-channel.label = Lataustilastokuukausi -channel-type.mybmw.sunroof-channel.label = Kattoluukku - -channel-type.mybmw.timer1-day-fri-channel.description = Perjantaina ajastin 1 -channel-type.mybmw.timer1-day-fri-channel.label = T1 Perjantai -channel-type.mybmw.timer1-day-mon-channel.description = Maanantaina ajastin 1 -channel-type.mybmw.timer1-day-mon-channel.label = T1 Maanantai -channel-type.mybmw.timer1-day-sat-channel.description = Lauantaina ajastin 1 -channel-type.mybmw.timer1-day-sat-channel.label = T1 Lauantai -channel-type.mybmw.timer1-day-sun-channel.description = Sunnuntaina ajastin 1 -channel-type.mybmw.timer1-day-sun-channel.label = T1 Sunnuntai -channel-type.mybmw.timer1-day-thu-channel.description = Torstaina ajastin 1 -channel-type.mybmw.timer1-day-thu-channel.label = T1 Torstai -channel-type.mybmw.timer1-day-tue-channel.description = Tiistaina ajastin 1 -channel-type.mybmw.timer1-day-tue-channel.label = T1 Tiistai -channel-type.mybmw.timer1-day-wed-channel.description = Keskiviikkona ajastin 1 -channel-type.mybmw.timer1-day-wed-channel.label = T1 Keskiviikko -channel-type.mybmw.timer1-departure-channel.description = Lähtöaika normaalille aikatauluajastimelle 1 -channel-type.mybmw.timer1-departure-channel.label = T1 Lähtöaika -channel-type.mybmw.timer1-departure-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.timer1-enabled-channel.description = Ajastin 1 käytössä -channel-type.mybmw.timer1-enabled-channel.label = T1 käytössä -channel-type.mybmw.timer2-day-fri-channel.description = Perjantaina ajastin 2 -channel-type.mybmw.timer2-day-fri-channel.label = T2 Perjantai -channel-type.mybmw.timer2-day-mon-channel.description = Maanantaina ajastin 2 -channel-type.mybmw.timer2-day-mon-channel.label = T2 Maanantai -channel-type.mybmw.timer2-day-sat-channel.description = Lauantaina ajastin 2 -channel-type.mybmw.timer2-day-sat-channel.label = T2 Lauantai -channel-type.mybmw.timer2-day-sun-channel.description = Sunnuntaina ajastin 2 -channel-type.mybmw.timer2-day-sun-channel.label = T2 Sunnuntai -channel-type.mybmw.timer2-day-thu-channel.description = Torstaina ajastin 2 -channel-type.mybmw.timer2-day-thu-channel.label = T2 Torstai -channel-type.mybmw.timer2-day-tue-channel.description = Tiistaina ajastin 2 -channel-type.mybmw.timer2-day-tue-channel.label = T2 Tiistai -channel-type.mybmw.timer2-day-wed-channel.description = Keskiviikkona ajastin 2 -channel-type.mybmw.timer2-day-wed-channel.label = T2 Keskiviikko -channel-type.mybmw.timer2-departure-channel.description = Lähtöaika normaalille aikatauluajastimelle 2 -channel-type.mybmw.timer2-departure-channel.label = T2 Lähtöaika -channel-type.mybmw.timer2-departure-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.timer2-enabled-channel.description = Ajastin 2 käytössä -channel-type.mybmw.timer2-enabled-channel.label = T2 käytössä -channel-type.mybmw.timer3-day-fri-channel.description = Perjantaina ajastin 3 -channel-type.mybmw.timer3-day-fri-channel.label = T3 Perjantai -channel-type.mybmw.timer3-day-mon-channel.description = Maanantaina ajastin 3 -channel-type.mybmw.timer3-day-mon-channel.label = T3 Maanantai -channel-type.mybmw.timer3-day-sat-channel.description = Lauantaina ajastin 3 -channel-type.mybmw.timer3-day-sat-channel.label = T3 Lauantai -channel-type.mybmw.timer3-day-sun-channel.description = Sunnuntaina ajastin 3 -channel-type.mybmw.timer3-day-sun-channel.label = T3 Sunnuntai -channel-type.mybmw.timer3-day-thu-channel.description = Torstaina ajastin 3 -channel-type.mybmw.timer3-day-thu-channel.label = T3 Torstai -channel-type.mybmw.timer3-day-tue-channel.description = Tiistaina ajastin 3 -channel-type.mybmw.timer3-day-tue-channel.label = T3 Tiistai -channel-type.mybmw.timer3-day-wed-channel.description = Keskiviikkona ajastin 3 -channel-type.mybmw.timer3-day-wed-channel.label = T3 Keskiviikko -channel-type.mybmw.timer3-departure-channel.description = Lähtöaika normaalille aikatauluajastimelle 3 -channel-type.mybmw.timer3-departure-channel.label = T3 Lähtöaika -channel-type.mybmw.timer3-departure-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.timer3-enabled-channel.description = Ajastin 3 käytössä -channel-type.mybmw.timer3-enabled-channel.label = T3 käytössä -channel-type.mybmw.timer4-day-fri-channel.description = Perjantaina ajastin 4 -channel-type.mybmw.timer4-day-fri-channel.label = T4 Perjantai -channel-type.mybmw.timer4-day-mon-channel.description =Maanantaina ajastin 4 -channel-type.mybmw.timer4-day-mon-channel.label = T4 Maanantai -channel-type.mybmw.timer4-day-sat-channel.description = Lauantaina ajastin 4 -channel-type.mybmw.timer4-day-sat-channel.label = T4 Lauantai -channel-type.mybmw.timer4-day-sun-channel.description = Sunnuntaina ajastin 4 -channel-type.mybmw.timer4-day-sun-channel.label = T4 Sunnuntai -channel-type.mybmw.timer4-day-thu-channel.description = Torstaina ajastin 4 -channel-type.mybmw.timer4-day-thu-channel.label = T4 Torstai -channel-type.mybmw.timer4-day-tue-channel.description = Tiistaina ajastin 4 -channel-type.mybmw.timer4-day-tue-channel.label = T4 Tiistai -channel-type.mybmw.timer4-day-wed-channel.description =Keskiviikkona ajastin 4 -channel-type.mybmw.timer4-day-wed-channel.label = T4 Keskiviikko -channel-type.mybmw.timer4-departure-channel.description = Lähtöaika normaalille aikatauluajastimelle 4 -channel-type.mybmw.timer4-departure-channel.label = T4 Lähtöaika -channel-type.mybmw.timer4-departure-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.timer4-enabled-channel.description = Ajastin 4 käytössä -channel-type.mybmw.timer4-enabled-channel.label = T4 käytössä -channel-type.mybmw.trunk-channel.label = Tavaratila -channel-type.mybmw.window-driver-front-channel.label = Kuljettajan ikkuna -channel-type.mybmw.window-driver-rear-channel.label = Kuljettajan puoleinen takaikkuna -channel-type.mybmw.window-end-channel.description = Lataustapahtuman päättymisaika -channel-type.mybmw.window-end-channel.label = Latausikkunan päättymisaika -channel-type.mybmw.window-end-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.window-passenger-front-channel.label = Matkustajan ikkuna -channel-type.mybmw.window-passenger-rear-channel.label = Matkustajan puoleinen takaikkuna -channel-type.mybmw.window-start-channel.description = Lataustapahtuman alkamisaika -channel-type.mybmw.window-start-channel.label = Latauksen aloitusaika -channel-type.mybmw.window-start-channel.state.pattern = %1$tH:%1$tM -channel-type.mybmw.windows-channel.label = Ikkunoiden tila From b4c4638a6bac8bd7c9ea78eeb94f031c46810370 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 30 Oct 2023 21:51:50 +0100 Subject: [PATCH 24/64] [mybmw] remove commented code Signed-off-by: Martin Grassl --- .../backend/JsonStringDeserializerTest.java | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java index 86668c1823ada..aafa5f3aa0265 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializerTest.java @@ -46,9 +46,6 @@ void testGetChargeSessions() { String content = FileReader.fileToString("responses/BEV/charging_sessions.json"); ChargingSessionsContainer chargeSessionsContainer = JsonStringDeserializer.getChargingSessions(content); assertNotNull(chargeSessionsContainer); - - // String jsonString = gson.toJson(chargeSessionsContainer); - // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); } @Test @@ -56,9 +53,6 @@ void testGetChargeStatistics() { String content = FileReader.fileToString("responses/BEV/charging_statistics.json"); ChargingStatisticsContainer chargeStatisticsContainer = JsonStringDeserializer.getChargingStatistics(content); assertNotNull(chargeStatisticsContainer); - - // String jsonString = gson.toJson(chargeStatisticsContainer); - // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); } @Test @@ -66,9 +60,6 @@ void testGetExecutionStatus() { String content = FileReader.fileToString("responses/MILD_HYBRID/remote_service_status.json"); ExecutionStatusContainer executionStatusContainer = JsonStringDeserializer.getExecutionStatus(content); assertNotNull(executionStatusContainer); - - // String jsonString = gson.toJson(executionStatusContainer); - // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); } @Test @@ -76,9 +67,6 @@ void testGetExecutionError() { String content = FileReader.fileToString("responses/MILD_HYBRID/remote_service_error.json"); ExecutionStatusContainer executionStatusContainer = JsonStringDeserializer.getExecutionStatus(content); assertNotNull(executionStatusContainer); - - // String jsonString = gson.toJson(executionStatusContainer); - // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); } @Test @@ -88,9 +76,6 @@ void testGetVehicleBaseList() { assertNotNull(vehicleBases); assertFalse(vehicleBases.isEmpty()); assertEquals(1, vehicleBases.size()); - - // String jsonString = gson.toJson(vehicleBases); - // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); } @Test @@ -109,9 +94,6 @@ void testGetVehicleStateMILDHYBRID() { String content = FileReader.fileToString("responses/MILD_HYBRID/vehicles_state.json"); VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(content); assertNotNull(vehicleStateContainer); - - // String jsonString = gson.toJson(vehicleStateContainer); - // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); } @Test @@ -119,9 +101,6 @@ void testGetVehicleStatePHEV() { String content = FileReader.fileToString("responses/PHEV/vehicles_state.json"); VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(content); assertNotNull(vehicleStateContainer); - - // String jsonString = gson.toJson(vehicleStateContainer); - // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); } @Test @@ -129,8 +108,5 @@ void testGetVehicleStateICE() { String content = FileReader.fileToString("responses/ICE/vehicles_state.json"); VehicleStateContainer vehicleStateContainer = JsonStringDeserializer.getVehicleState(content); assertNotNull(vehicleStateContainer); - - // String jsonString = gson.toJson(vehicleStateContainer); - // assertEquals(content.replace(" ", ""), jsonString.replace(" ", "")); } } From 7f3195fd1a506aacfc62e4e912e2f8a02cf9422c Mon Sep 17 00:00:00 2001 From: mgrassl Date: Tue, 31 Oct 2023 20:00:05 +0100 Subject: [PATCH 25/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java Co-authored-by: Jacob Laursen Signed-off-by: mgrassl --- .../binding/mybmw/internal/discovery/VehicleDiscovery.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java index e417fa8acb5c8..b4870ee2252cc 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java @@ -64,9 +64,9 @@ public VehicleDiscovery() { @Override public void setThingHandler(ThingHandler handler) { - if (handler instanceof MyBMWBridgeHandler) { + if (handler instanceof MyBMWBridgeHandler bridgeHandler) { logger.trace("xxxVehicleDiscovery.setThingHandler for MybmwBridge"); - bridgeHandler = Optional.of((MyBMWBridgeHandler) handler); + bridgeHandler = Optional.of(bridgeHandler); bridgeHandler.get().setVehicleDiscovery(this); bridgeUid = Optional.of(bridgeHandler.get().getThing().getUID()); } From b3562c9b09eb01d71313cd73a3391160d4958662 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 31 Oct 2023 22:12:42 +0100 Subject: [PATCH 26/64] [mybmw] revert previous change Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/discovery/VehicleDiscovery.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java index b4870ee2252cc..e417fa8acb5c8 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java @@ -64,9 +64,9 @@ public VehicleDiscovery() { @Override public void setThingHandler(ThingHandler handler) { - if (handler instanceof MyBMWBridgeHandler bridgeHandler) { + if (handler instanceof MyBMWBridgeHandler) { logger.trace("xxxVehicleDiscovery.setThingHandler for MybmwBridge"); - bridgeHandler = Optional.of(bridgeHandler); + bridgeHandler = Optional.of((MyBMWBridgeHandler) handler); bridgeHandler.get().setVehicleDiscovery(this); bridgeUid = Optional.of(bridgeHandler.get().getThing().getUID()); } From 2d12e3032512880ac313c9df0938ebd7acaae68e Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 31 Oct 2023 22:14:22 +0100 Subject: [PATCH 27/64] [mybmw] changed time conversion Signed-off-by: Martin Grassl --- .../mybmw/internal/MyBMWHandlerFactory.java | 23 +++++++++++------ .../internal/handler/VehicleHandler.java | 22 +++++++++------- .../mybmw/internal/utils/Converter.java | 25 +++++-------------- .../internal/utils/VehicleStatusUtils.java | 3 ++- .../mybmw/internal/dto/StatusWrapper.java | 11 +++++--- .../dto/charge/ChargingStatisticsTest.java | 7 +++++- .../internal/handler/VehicleHandlerTest.java | 6 ++++- .../mybmw/internal/utils/ConverterTest.java | 12 ++++++--- 8 files changed, 62 insertions(+), 47 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java index 881d6b5a8ca41..fe4022a09a4ef 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWHandlerFactory.java @@ -12,7 +12,8 @@ */ package org.openhab.binding.mybmw.internal; -import static org.openhab.binding.mybmw.internal.MyBMWConstants.*; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUPPORTED_THING_SET; +import static org.openhab.binding.mybmw.internal.MyBMWConstants.THING_TYPE_CONNECTED_DRIVE_ACCOUNT; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -21,6 +22,7 @@ import org.openhab.binding.mybmw.internal.handler.VehicleHandler; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.LocationProvider; +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; @@ -44,15 +46,19 @@ public class MyBMWHandlerFactory extends BaseThingHandlerFactory { private final HttpClientFactory httpClientFactory; private final MyBMWCommandOptionProvider commandOptionProvider; private final LocationProvider locationProvider; + private final TimeZoneProvider timeZoneProvider; private String localeLanguage; @Activate - public MyBMWHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference MyBMWCommandOptionProvider cop, - final @Reference LocaleProvider localeP, final @Reference LocationProvider locationP) { - httpClientFactory = hcf; - commandOptionProvider = cop; - locationProvider = locationP; - localeLanguage = localeP.getLocale().getLanguage().toLowerCase(); + public MyBMWHandlerFactory(final @Reference HttpClientFactory httpClientFactory, + final @Reference MyBMWCommandOptionProvider commandOptionProvider, + final @Reference LocaleProvider localeProvider, final @Reference LocationProvider locationProvider, + final @Reference TimeZoneProvider timeZoneProvider) { + this.httpClientFactory = httpClientFactory; + this.commandOptionProvider = commandOptionProvider; + this.locationProvider = locationProvider; + this.timeZoneProvider = timeZoneProvider; + this.localeLanguage = localeProvider.getLocale().getLanguage().toLowerCase(); } @Override @@ -66,7 +72,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { if (THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(thingTypeUID)) { return new MyBMWBridgeHandler((Bridge) thing, httpClientFactory, localeLanguage); } else if (SUPPORTED_THING_SET.contains(thingTypeUID)) { - return new VehicleHandler(thing, commandOptionProvider, locationProvider, thingTypeUID.getId()); + return new VehicleHandler(thing, commandOptionProvider, locationProvider, timeZoneProvider, + thingTypeUID.getId()); } return null; } diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java index a41dc3c184045..b28406853d1fb 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java @@ -138,6 +138,7 @@ import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils; import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils; import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.net.http.HttpUtil; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; @@ -192,6 +193,7 @@ public class VehicleHandler extends BaseThingHandler { private MyBMWCommandOptionProvider commandOptionProvider; private LocationProvider locationProvider; + private ZoneId timeZone; // Data Caches private Optional vehicleStatusCache = Optional.empty(); @@ -205,12 +207,14 @@ public class VehicleHandler extends BaseThingHandler { private ImageProperties imageProperties = new ImageProperties(); - public VehicleHandler(Thing thing, MyBMWCommandOptionProvider cop, LocationProvider lp, String driveTrain) { + public VehicleHandler(Thing thing, MyBMWCommandOptionProvider commandOptionProvider, + LocationProvider locationProvider, TimeZoneProvider timeZoneProvider, String driveTrain) { super(thing); logger.trace("xxxVehicleHandler.constructor {}, {}", thing.getUID(), driveTrain); - commandOptionProvider = cop; - locationProvider = lp; - if (lp.getLocation() == null) { + this.commandOptionProvider = commandOptionProvider; + this.timeZone = timeZoneProvider.getTimeZone(); + this.locationProvider = locationProvider; + if (locationProvider.getLocation() == null) { logger.debug("Home location not available"); } @@ -471,9 +475,9 @@ private void updateVehicleOverallStatus(VehicleState vehicleState, @Nullable Str updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL, Converter.toTitleCase(vehicleState.getOverallCheckControlStatus()), channelToBeUpdated); updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE, - Converter.zonedToLocalDateTime(vehicleState.getLastUpdatedAt()), channelToBeUpdated); - updateChannel(CHANNEL_GROUP_STATUS, LAST_FETCHED, Converter.zonedToLocalDateTime(vehicleState.getLastFetched()), - channelToBeUpdated); + Converter.zonedToLocalDateTime(vehicleState.getLastUpdatedAt(), timeZone), channelToBeUpdated); + updateChannel(CHANNEL_GROUP_STATUS, LAST_FETCHED, + Converter.zonedToLocalDateTime(vehicleState.getLastFetched(), timeZone), channelToBeUpdated); updateChannel(CHANNEL_GROUP_STATUS, DOORS, Converter.toTitleCase(vehicleState.getDoorsState().getCombinedState()), channelToBeUpdated); updateChannel(CHANNEL_GROUP_STATUS, WINDOWS, @@ -656,8 +660,8 @@ private void selectService(int index, @Nullable String channelToBeUpdated) { channelToBeUpdated); updateChannel(CHANNEL_GROUP_SERVICE, DETAILS, StringType.valueOf(serviceEntry.getDescription()), channelToBeUpdated); - updateChannel(CHANNEL_GROUP_SERVICE, DATE, Converter.zonedToLocalDateTime(serviceEntry.getDateTime()), - channelToBeUpdated); + updateChannel(CHANNEL_GROUP_SERVICE, DATE, + Converter.zonedToLocalDateTime(serviceEntry.getDateTime(), timeZone), channelToBeUpdated); if (serviceEntry.getMileage() > 0) { updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE, diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java index fe46695f55ccb..69cdd02711010 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/Converter.java @@ -12,13 +12,10 @@ */ package org.openhab.binding.mybmw.internal.utils; -import java.text.SimpleDateFormat; +import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -40,21 +37,15 @@ public interface Converter { static final Logger LOGGER = LoggerFactory.getLogger(Converter.class); - static final String DATE_INPUT_PATTERN_STRING = "yyyy-MM-dd'T'HH:mm:ss"; - static final DateTimeFormatter DATE_INPUT_PATTERN = DateTimeFormatter.ofPattern(DATE_INPUT_PATTERN_STRING); - static final DateTimeFormatter LOCALE_ENGLISH_TIMEFORMATTER = DateTimeFormatter.ofPattern("hh:mm a", - Locale.ENGLISH); - static final SimpleDateFormat ISO_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"); - static final String SPLIT_HYPHEN = "-"; static final String SPLIT_BRACKET = "\\("; - static State zonedToLocalDateTime(@Nullable String input) { + static State zonedToLocalDateTime(@Nullable String input, ZoneId timezone) { if (input != null && !input.isEmpty()) { try { - String dateString = ZonedDateTime.parse(input).withZoneSameInstant(ZoneId.systemDefault()) - .toLocalDateTime().format(Converter.DATE_INPUT_PATTERN); - return DateTimeType.valueOf(dateString); + String localTimeString = Instant.parse(input).atZone(timezone) + .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + return DateTimeType.valueOf(localTimeString); } catch (Exception e) { LOGGER.debug("Unable to parse date {} - {}", input, e.getMessage()); return UnDefType.UNDEF; @@ -175,11 +166,7 @@ static State getConnectionState(boolean connected) { } static String getCurrentISOTime() { - Date date = new Date(System.currentTimeMillis()); - synchronized (ISO_FORMATTER) { - ISO_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC")); - return ISO_FORMATTER.format(date); - } + return ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT); } static String getTime(Time t) { diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java index 426ba472cc53c..aa3a97ab4cb24 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/utils/VehicleStatusUtils.java @@ -13,6 +13,7 @@ package org.openhab.binding.mybmw.internal.utils; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -55,7 +56,7 @@ public static State getNextServiceDate(List requiredServices) { if (serviceDate.equals(farFuture)) { return UnDefType.UNDEF; } else { - return DateTimeType.valueOf(serviceDate.format(Converter.DATE_INPUT_PATTERN)); + return DateTimeType.valueOf(serviceDate.format(DateTimeFormatter.ISO_INSTANT)); } } diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java index 2c7d125e3f600..afd79fc233af4 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/StatusWrapper.java @@ -72,6 +72,7 @@ import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_PASSENGER_FRONT; import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_PASSENGER_REAR; +import java.time.ZoneId; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -336,13 +337,15 @@ private void checkResult(ChannelUID channelUID, State state) { case LAST_UPDATE: assertTrue(state instanceof DateTimeType); dtt = (DateTimeType) state; - State expectedUpdateDate = Converter.zonedToLocalDateTime(vehicleState.getLastUpdatedAt()); + State expectedUpdateDate = Converter.zonedToLocalDateTime(vehicleState.getLastUpdatedAt(), + ZoneId.systemDefault()); assertEquals(expectedUpdateDate.toString(), dtt.toString(), "Last Update"); break; case LAST_FETCHED: assertTrue(state instanceof DateTimeType); dtt = (DateTimeType) state; - State expectedFetchedDate = Converter.zonedToLocalDateTime(vehicleState.getLastFetched()); + State expectedFetchedDate = Converter.zonedToLocalDateTime(vehicleState.getLastFetched(), + ZoneId.systemDefault()); assertEquals(expectedFetchedDate.toString(), dtt.toString(), "Last Fetched"); break; case GPS: @@ -473,7 +476,7 @@ private void checkResult(ChannelUID channelUID, State state) { } } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) { String dueDateString = vehicleState.getRequiredServices().get(0).getDateTime(); - State expectedDTT = Converter.zonedToLocalDateTime(dueDateString); + State expectedDTT = Converter.zonedToLocalDateTime(dueDateString, ZoneId.systemDefault()); assertEquals(expectedDTT.toString(), dtt.toString(), "First Service Date"); } } @@ -561,7 +564,7 @@ private void checkResult(ChannelUID channelUID, State state) { switch (gUid) { case CHANNEL_GROUP_SERVICE: String dueDateString = vehicleState.getRequiredServices().get(0).getDateTime(); - State expectedDTT = Converter.zonedToLocalDateTime(dueDateString); + State expectedDTT = Converter.zonedToLocalDateTime(dueDateString, ZoneId.systemDefault()); assertEquals(expectedDTT.toString(), dtt.toString(), "ServiceSate"); break; default: diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsTest.java index 18c5e08ca33bb..26e82da10c5e0 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/dto/charge/ChargingStatisticsTest.java @@ -22,6 +22,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.time.ZoneId; import java.util.List; import java.util.Map; import java.util.Optional; @@ -39,6 +40,7 @@ import org.openhab.binding.mybmw.internal.util.FileReader; import org.openhab.binding.mybmw.internal.utils.Constants; import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingUID; @@ -87,7 +89,10 @@ public void setup(String type, boolean imperial) { when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test")); MyBMWCommandOptionProvider myBmwCommandOptionProvider = mock(MyBMWCommandOptionProvider.class); LocationProvider locationProvider = mock(LocationProvider.class); - vehicleHandler = new VehicleHandler(thing, myBmwCommandOptionProvider, locationProvider, type); + TimeZoneProvider timeZoneProvider = mock(TimeZoneProvider.class); + when(timeZoneProvider.getTimeZone()).thenReturn(ZoneId.systemDefault()); + vehicleHandler = new VehicleHandler(thing, myBmwCommandOptionProvider, locationProvider, timeZoneProvider, + type); MyBMWVehicleConfiguration vc = new MyBMWVehicleConfiguration(); vc.setVin(Constants.ANONYMOUS); diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleHandlerTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleHandlerTest.java index 99752862e9a77..7f52423900d68 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleHandlerTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/VehicleHandlerTest.java @@ -24,6 +24,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -41,6 +42,7 @@ import org.openhab.binding.mybmw.internal.util.FileReader; import org.openhab.binding.mybmw.internal.utils.Constants; import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.PointType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.thing.ChannelUID; @@ -97,7 +99,9 @@ private void setup(String type, String vin) { MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class); LocationProvider locationProvider = mock(LocationProvider.class); when(locationProvider.getLocation()).thenReturn(HOME_LOCATION); - vehicleHandler = new VehicleHandler(thing, cop, locationProvider, type); + TimeZoneProvider timeZoneProvider = mock(TimeZoneProvider.class); + when(timeZoneProvider.getTimeZone()).thenReturn(ZoneId.systemDefault()); + vehicleHandler = new VehicleHandler(thing, cop, locationProvider, timeZoneProvider, type); MyBMWVehicleConfiguration vehicleConfiguration = new MyBMWVehicleConfiguration(); vehicleConfiguration.setVin(vin); diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java index 7ef363db4d643..8d97a6c012837 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/utils/ConverterTest.java @@ -15,6 +15,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.time.ZoneId; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; import org.openhab.core.library.types.DateTimeType; @@ -41,13 +43,15 @@ void testToTitleCase() { @Test void testDateConversion() { - State state = Converter.zonedToLocalDateTime(null); + State state = Converter.zonedToLocalDateTime(null, ZoneId.systemDefault()); assertTrue(state instanceof UnDefType); - state = Converter.zonedToLocalDateTime(""); + state = Converter.zonedToLocalDateTime("", ZoneId.systemDefault()); assertTrue(state instanceof UnDefType); - state = Converter.zonedToLocalDateTime("2023-01-18"); + state = Converter.zonedToLocalDateTime("2023-01-18", ZoneId.systemDefault()); assertTrue(state instanceof UnDefType); - state = Converter.zonedToLocalDateTime("2023-01-18T18:07:59.076Z"); + state = Converter.zonedToLocalDateTime("2023-01-18T18:07:59.076Z", ZoneId.systemDefault()); + assertTrue(state instanceof DateTimeType); + state = Converter.zonedToLocalDateTime("2023-10-28T17:41:17Z", ZoneId.systemDefault()); assertTrue(state instanceof DateTimeType); } } From dca468b70bcfab87c0f68cb08bb78f91909f959d Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 31 Oct 2023 22:17:06 +0100 Subject: [PATCH 28/64] [mybmw] delete unnecessary files Signed-off-by: Martin Grassl --- .../responses/MILD_HYBRID/340i_rearView.png | Bin 100351 -> 0 bytes .../MILD_HYBRID/340i_vehicleStatus.jpg | Bin 199877 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_rearView.png delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_vehicleStatus.jpg diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_rearView.png b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_rearView.png deleted file mode 100644 index e5723bb16684f529e5f7999a8904dd24ed4d5b53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100351 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 diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_vehicleStatus.jpg b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_vehicleStatus.jpg deleted file mode 100644 index 38be4bfe57f9849fb7eae6853390dd91a3451054..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 199877 zcmY(r2_V#K_&shJYr<$l_I*%clzq$Cg=8u%mM~J;)!4TPWy_WcDMC$)qJ_wwb&M-@ zrI00S8DuTn|M`sj{eQndSGKy$yzhC>bIy65SAva|$xco|P6h^soo1#uTLuQEcMJ@S zlI*PTm4VO#9tLKvL^GU$T_EGH`pYTjCazBY*($$;U+Vds(|MuX?2YNo0@uFb+hZ;l zM&{*CO&l?{;%i}cPTl9uBwJs7_fTpVuh=YGJ%bC1gd$n;u_o7y87u-*o_M}Gb z!Jq#qaKDf&Ox&j*{N91d7PnX_U8zMRk*8~__T7JU34I%W8cr%Qdp$>DiAxoJ z9v44ji2hElfv}c=xVk5Gm+KDtoWK*Kbev{)IIcFt#KwyHCdATT@|_|UO(HUidpz7D zA{_4)qp=ppdL9l83iAc7{LR!r)8fGGqEF<<{GUj^q8PEOO*C{qHN*__G<->ewGedE&zu1NQ{( z`NH-l>Qq#3q_|DhnywP7VBOie-SHN84({2zcavFqFa0GvKEsypt(2D;n}KD=8zRZ(SG(vw{E(^X~4QYPIB*XpX`91)>+OlB9!s-$fo zy-JpNr`SVNU$m8W@8`RJ;hEKt@LF10v&@_6ReBG}PxSqW<92(9Ed$Efal6c{s!`f2 zN@5jBK*PO8l#+OoN(xcN4q8%4x!41E=?O~;H$K=w-)Bg;-joM$Q z0PB2233Y8CY+vBcFa9gaLv@QEi!ACJ8hB1$a7JE+zdy=IyBD@>Kk)r~sYC(KqLJ(4 zZ!^nwuS>`}=qbiCy;n;zJ{~9vi1fk*NF0?c7^O|q@}>g5k}PF}iM_}NRweK`8U0c< z&ng7neJkDp7f{mk*ckUV<6{#0fucoo%s~7kF2)!K-w0C_AbFI5(H3JQA;HM>Fn*FN zuCe>|<=%HWIXU?Ay$ZCN0&*CE!i61G!W-c|=o?CZ>T0(5J>;8J{@GP7glqwC6DnzT zA9mEik4hQKJ&x}W83rc6>3p|<#xmEiZE z9ZF9fIB?(@Wzp{SDRX->%z*^cy|<-ddCuS8j}}A^*qReH^OhwFwDPPDOMDv8>X~|l z&9nM2ptZKPcAhwki^1V!B{XRV(cu7QPoAFW-INV59(}4s- zoD?IxL7~ma?Uug8zQMuenHf*za#unE>D_Z>EPeL@0B7$3O4BdoI8o z_vvcqN&`h*af0&AnE=aK83&Ak#~<^jI^aK}_!-rrgq`*WqAOU>u{ijw+=&v#4=~Hn zbXgh(MgdB2Uf+tnd;u?Wl4C0oTPx}D0sZHnkf#@e-zUdEA}tvgc9&JH_)|4|+BAr3 z8=JwUy&?BK?!vKUeahGp^*R3Z-rH{Isp-V3(sv;V@jqx4Avfby1(Q&dcKAK-9a7$k z8KL4#alB{`b)oj9pS@aytVooUSfv)0!?gM!W5QX!R0ft9wFl%1tpmi8G1`yZHjN(g zPeH69o`l6tziX!ObI1M2^%$eS;sI88JN$>ge>GFIf3xr3zyIm;=fCTid2H}|Op=A1 zVL8cUb;TmwlZ>MZ$lHd2yT$bb0FFoUE2FeuGczyWzHOn=4#Li=9y9Dw%@zm=2~j=P zxVfndUrIEVywNDG=GE^+L=HHAk}>kAT7Z!Hg^4YcizImMcuXk(bGTnO^Ly9;b!Synr{0?;JcYuc#=LMrZw1gI!{bOs3Sz zbblIy@O@_X)C%bh>g+m|lrC$n`=Y8!p{lB?BBTP=vVdGuQ*+NMSH0UBbAVWNe$QI==u5D@QwEpws~5_=Nf)LT0g#YY5CpWZr(dkMH}>AaljO@Ed~%gJj^0@ zqaXMFJWj~QypydUfPhg%;WXH$;0S5;|uvxKgv? zyEKj8oEP1?T^hdhq(Y09Oi~u&2Kw`(zM|&qb+x6-YJ~k&b?>@2w*F^9d#;XINH09M z-l+~A_~f;!cy{lH84WAsj0T>gJ*xfHLFoen=P8NvftEe0uq(!eXxJx!ddHz!3YE%T z*d5<}5qG{9{tAJJ5*FbcFAaBYEcT3s`x!#>fFIQkA4|3x2rD3mXiW6lEoG9)UfF%c zeZoq5e)8i9UUtYRkB&3R@)Vv3U81hD2QmkPsPQ3kg>=`Wf$3zAXWB8ZT zOTiB_OflU*mjboSF+y3kBb^zxaz$bi)p(5bnWEjc=G6j3cW>{MlQ5gUOkUR*58l%BUdA2SusU#Q$0mznSVCq``VW;Uxp^T3m4bIva+&x z(Su}LPH!VjtmrFvkH^H?a`sGR6nkV_%2a3_>-k4@=jeQP_r<2cL6Op$zw%J#xZ(_f zm%jA%IbXhf>t}B%!s&!&XAO5+3!RA0Q7g0iq$c`*8XKvSYw~6R6eQW%kNa_22D~Fwgzc5glTcTWmV{#di89M6t zQlqXAI86J~D2ciLDhQ@1QUm~&UFOo#RUl7tI}pM~cz(HiS8<$Zcs7|lxv~aN@8rpo z@ByMO{LWr52J`~tQ*8?}Z7)2}XmG|zb1M`n-J!4U#iESWa5K#OMw)Fw#!>TxN?T5N z*4AOuCqhSdicN3H-DtM+`;uJwPtH}=j(Y^`bdj-Up-?pT!=nkT#k z(%HzcU3ETXKKx7$P8ggn*d}oaSWn;|X9A%3)}fkKD4P}6W(3ckJ$uE|bC~w&Y7%Lf zmM!2xu5sa|B?5b^20u(s%PS}-{K&O;Z~*>D$l&uY+D$6~BDFP#l0+%W zsK~eF#7GZ5&*0O$h>@;2h2Zg(*I;$qgx*%r=E$MV=qIO2N8qWzQ~Qw{1ji|8BtbUt zzdxrP9cAgnydMKwLEnd>-Gi2x*a&Zo^yV{jOto;)Zos%qItLJqrOYlsT7~Z@Yj3IJ zM03*qE8gDzqs=>krGX|tT|IWbH(THU!pEQ&q4+6dx%YFiLV-wRG{g_IYeei{-G_f) zK+d6s3s*Q3s%Tz7#&^r+BoEM$=R#|U-68BGTrosTVb+_W%_^1e9NPVYNp8i7(0_~6 z-L6qyQ)7j!*&{M1dU#_tLeC^cWb8*)3Sb8hP|6?d_cK-Qwuqr{XLo7T^eJXPe z-rlyAD8iY;7Q)}oUwfDlC<#kTXAqjKM7C84)V+(U1BKGWkL=qp`+>Gaj#Iw{(;2z)~|t12X+a+f1vP-9Fip z(%b7h3D0bnA&gkdu>PiMi)-%U`CgN#J^KFC1OaR#Qcu05CMeC#te}2!bwaX|l6D?+ zsBTTzC94i63SfvsK~;ayGxg6@fDIlO(?|#4#s7am>_y)%z%{%IorKTP z25@`>G(QZ0KnD2*I78P5Xi6iiWm$4UB6=#o92Oa13HYQ@!4{~Hd`_Elpv7UK>2G1L zGWd#iOGE=1y!}@6eYWT4(&;m2E_irw;EQ^H`u=D|zZ*E1XBB5vay=gEEwVu;kv;O3 zDr!25H-CUPhwnw-adcD_mSoAF((-F|)_-CA*u99&pd)H(ef|AE{x1DIR~w!O2koN@ zP&{B&`Huhi8_YjktjuQ}9c!m(-%oxneKL)uZMqR_$7$a*+;cdHhv-UV-NsxIUe@`8 z*5_MND%K?HysOqq->rpoUa32e)X{K1>eJFv|IPKFlc!E0wSz}{j-kXVZzOLvR2JS} zxOz3yq0|HudlC~{m1+B*+ydqUwAVuY2uWb?&+`C~%EdC#VHhcO6vTs;FdPjiWA~;Tg!z{?@*>R}4ZlO!B3sf)c*~90T!PP#$d0hNGsE3a_U13?balMOJOarioYPxCWsU%^P^HfCC;PYZM{RGI>e@X1#)P;YK zr^$2M-+l zihuVN77>-C~@!#ok$Y$Ab? zfH`1+ehd0LiV+HwU|ycEL-3Sz&C)aP`A+Zfbif1nqrQQG4-_Bb8Jd*^-u~>_!rq?` z$cw=`!S6-hJMe7p;v;!@=>PDcaWV1wxWFe8(I?OMLNWFQk!+V4FDBtb1gywP#Ipmu z&<3Twh0+qQ;QtVtNQ{5+?wxt<_Y$&vvanV&NLU0gWO<;*pdc{y0G6jup8{!Z^Kr#l z^8__p^Whh^oM1u#yH84M+JRfaHCN9y?6~6PRaRNqmb70DtPY|B<_U@3D zS^zRQa^#3QktT%5?kF~qq1E}(r_Y}GE>0d#7SVyi58w~b+@?PO2J!(N9&pnBxycS_ z$*|3?3R;j+`+0X`BkYQHGv&p6Hc%keL1eFIQRn^fO&9bWL9J;=D&8|btkuC7A zDj2KLWVM~NWRoFUAPPJJSDcnQmgz`Mc3cv!OjykVc#Z7rHb|FXnU3%rIUE}oq>4r@cRg4F=y6cumNdl+gS2onb{sWS_-w$>m9XB_(Sg*IYph6&Y8BX67Q;agu-~ZMJaA>eAP@+n!e{&vszsUD} zbNOv$=pU6+r4DYO%!hsAN%O{t0bu@8O1`8v5h^*cevbMFGo(ORF1>o*u_|CHt$c3w zAiY*gPL6!I6-JCGsoE-~Z94vZr|Uwn&~X2^w-?mzD?h6C@)ws}y@-1zB0({1qM@m; zUmy0P251T+f&-*Z!#Tt_AcYT}ceb%oo)wS*&XQzxT`p>7|DoEl&dwaQ$1BTU_RqXj z`|AFxHf1J-A2{qZVlW6d4g*|*5CByO(2|*H+}t;=YaQ^Ph{U>N`|c}rqwufX5%)hm z{vuaH>bjrF$+4@P?NLw7Esoo8%W-m=TNr5Emy?sj7Cd5)_tDo!^BNfOG6I^=^j*T) z=mW0oLLPBKjTmYloA6QdvB8tcBiBXO#YF$Knt&!P0Wt6&G{%vbVE&nR*sYTM@Vkd1 z{&Ya86~-36e)p~&Tr8{xAfxuX2|j@xvIPo+1la;~n}on35|Dvxu-Z-}H}I$gA6zvW zS>>Xx?$qpjy`I6pW%_Ovl6l!>Hu_N?$&+uA)BKG=QXp`mKVB$QAazgxJ~SH03LOQu zphAmajDu{T1N1s%Dh{PBpwcg2zKopZ^;O@0|NR%9f;&)8QIYijRTm>(bdTe68WBZB zySZ<%7x!ScVdD_Cgnxd%s(GyA4g#4j-8DtPKh#PRqV@{|%fh{%cn&b4lO$_#@lK=8Ad&Kou`cDL| zRDJJ+_v-%RTs%69ykv9a)F9M?l=KQvI6lPwJP2FVTqud%HOhiXk5(=%gXO2kG1J{5 zh}Um@(X52TU-PWQ7M(ete&Ln6#`=0+A0HthAy{UT7F(`Bi5pGGr_inWd}5ZeOLZj| zsggo$P1V3*{p~Y*F>-7wob8N8bfB>99*d-Nkh8Id*nM_(Yy+dSv5m8FpnrW7L&=_h z)K$45hx#)Yr$<4=NOz$LrpV{*wz`*yKIIS;i)0+yOP@}JE`i7J2H?_2)-u}Mg_z!8yh@3mhuvo32ccbi`n*S~+@Aw+DE^zruL>CXdBx!6$8 zoKn{)4jV7*-YiT+ww}J_@FrB2C0DEeq5!tpvw43<{{{WxOl+L14Xu@2ZDpX5iub_K z_rB5)`G~VUQK?V}C_+Cb%P&VsQi#_khPgpF+UGQ{5IeUzW4)z*WUr);3I5Lq$8{s! z-?vkIraxh5MG%r9(gQToy`XUcx^!Lau`Vw!Z$z>!kjX&kATTJ2u!sW#paZA7<8KQP zj6H84#~+*QtHu&bIs2f0q*`GAzfR!tp&vkKh79=NL?UCQEnw1b-&mwpav??i?W5@H zIo-DSGl+uy*sf7X7Ahw9->T}FT>(mq0r8E;=jw*tb!kcV9j6FDL|6~ZaJ@dV&?M#K z%r&U#psyW3kem9Z0JMJ8EB>4LyG6`x21e(g&Gr&yvXHUq% z56I-wrL~PNrmca2+D%edC?O?w_6a>}Pl>#>@6e_-Vc8kGc8IV{Ba`1D(Z`?Hj%$Yp z*9KRR6!t}RwWR{$m3}2HKl-VIq|_^y9KOA8>-;Mti3Q8l>Do{sfb|l*JD&|(*fKEB z-jLS}yW8(e)P};*;L9Vy#M7c9#*|Vwz}T`M*<&`g7UZNW3H^7%35kuARVx9d!P7S= z&H@?C_L*mQXMD?e+W&w|c`WZwzFBZTGO|F7iAg^CW+c1VBX$W2J5wME-Zl_9qqD^N z#r5ZIL`M$k^CH?r+uaN_Uy7mop7fN4RR zCpx`k3BzkLbYeHWc=@tL?dpd^+y@;5boZt@Pe==g&XK2Ui@l1 zVbfhdDtQTpj#OCBu|twBizw^p_&P~;*mZVUZT4vPc=j^}Py{1G413fGH`H!0)2uoM zG=}<*S4C<0k5d`d&Jg-PTog$4aBGbOc0Y){gMI5}qn}C1jrJ3JqEaF!$7YwQVPs%H zI$l&%Aa-9OGBWZe1Czuf_J%}|XbkY|Q>wYijMlKflds*vyuFxoe zgEak)KqT(N79J^Vt`}x5liwn6%X#1Si}SnX-vKJ@Hu$tevdY`{?~h$p;=fORWw*!Q z@X1Y$u_I%;8Ch`bdB^U-5$Mkg4lxz6d-bWFJ25Y>6TwJ7Nkhr|i06;ynB4so}57_v+4jm4hOF$Krr zKKFb8t!?jNdP08X%9Vcjrd!^ny=z-`OMUVD;nOGb>ZIG_RamH<=o?4%F@LKKJo0sRt)GU6J9h*OajMl@K42P(kM-JxIzn#W1xD) zD5U}vlYbo}OQ$(Fb12e7mYuee(EWejgnJT-b$uwn=VE*59vD~${)B>@8D zSKuzrv?t0R02ssyu%PWwC_5A{uK^mTkJH!Z#i5*yQRoNY;czio|M2nhdhKG2Icbfu z6OV4*U+v2j&mKRd&%l^H{I8FnXOiRQnB(}%77O4x;nK}eUNpKu&+YuU#ycVha--jwrJ=D1lmWeO-n>!5Leu2)lT+5*tk<_(l=#;myno(cd5eqLOb1w z@GHrf{<^HRzh@PdudaJlhSn5t)4HTWi!nwEED;!hBrO^kfRJq&3?2ytj{2ZFEjjM` z_0b{34^V~gV)1%~m2p+&K^vL`R%&9m$|$c3HCwrrM6#szXBkvBUMkE4M2zp1R(Ok% zdyBE(_5=Wntb+E$=;)i#krGH*U}Be#X2;T9TVy-~yr+h_P4u z3KARg#NwS~UJM8dxD{gpAf47bzpw8UUHj7FLqt?B7{LGO z6=Vx=f1=$NDv0!tyFT1+ZCq&>l*injV`EceVQj!S!R;Rh`7f+DofC-e5WVJN@%RVi zR}|qB?l$;PH8(2_T;;8gwzPu&VgUHq5u?~%4lJj-{LMJS$RsX23y~uH!bM{6%WH0Z`;$I`cn(@!52EI2Siru1`GR;pC`~S-%A>{3{fY2T zAmCITWxd>tG!&osw&buv?e`@%2*bMsAMNNlF^M^!21NSRGcT|b*@(FUL?I;CNroo~ zt(d3Wyn6(RZ=O5C;@#4~ax0HhDkR+rDjL{ygUVY>RA&K}u_K_o51G)`* zVGwMn)y(hIT_APMeL7+N@;!WGW&Z_e=H`n+4N;rp$=~?p?&vn``5!!17xcCk3IR)$ z#$!F}y`7z%j>06J{tD4P_4f}x{)}QTvCbQb2%I$i7E}2(?lkeY8E(F5G^A)?ZK|a75^%AQvkrT|)EtxD1Fl$dGa? zN2l#7MO zp6*^{cvzqE$op_ZLIrJmcO+OQK#8fTp+awl-Zc(v(LxM75Qq~nVR)@IqBNjQ(TL>U zh~%UGb& zNDx9^!DMi;xi@PJB`c3MGcvnS1N}+4jGeA{FG!bMcklLT$^9l|W7A5O16SUj_*-~4 zl41%R_wr@8nR!4KJ8HcLazP|-GP1!3SF*7mK=Yy-t}`=51CM~#&_E9-;ZV=m*@c)S zm{RU57kUUHsC;ZvA%Mrvw?wSBAhFtZt___-=y@QM6u%E`rCEpnPOs1cTleqWmOIa8 zgksgKW|y8`X;&3h=X}li8jR!Hju}5<<WvJiXQIVa1bI=Ls{u2m%_*Zc(L`@5A- zm9q=FA=GE&Ykj_a*RByFB7?QBU4t{2;b!K>TI8G3LiptpF=C@8lD#6Ar z>aN*=vfav-LVWC&BkE?0G5P^9sH0+%$}RACaN+&+>O(a6wq=Tl74ok6BLe{zoQ8|47gPmd_5_K3AuR`yivSrKA~2f) zGMU4y8sd0|Z*LaAz8N$9_U2&**t&0Ty%)Y-E}d`Q?``S*y=J5R_wV2Cj-m4(w#FbN z9Lj;i z2;y^I;JMdYXe$)Z8_e3E1Jxb0Vh|3rY7r)CCTj_o_-Rt3we|>?oG@V7(}J%)(dO&c zcGSaAsV;H0Rai-hKTEnuz$wzdDUX@exJ#+fC7V&jG62Pt9u;Gjjp9p7jWL5+j`p&q zn@qm?e5Sm}Ko9(6E9;Te)X8{nK7&J%@lN7mz=3dx9{&_1wp=c`Ygv~Eb_f|^uwHMn zhD6EpZ1UxAS);I=Nos>bY7q-+e;W4HL{@=G2HYT#I!uE3sTK%jEy`=shN{&@&noRr z3^7l}?oA{r4USX4C9)K{-0;nfVzg?Rs=RJ3W0%eWs<%vWiX`wJ_ypQ2VPTNKx)bai zq6AI~ey2FIrNOkXG>PGXiM@_PKqlLv^iY45W3Qn6$U?$EE zj(vX8+xPWrfuhB|wYeS8+w58JslEoDO_NksUe)O8&L#vk&dpv$5MdIb#}L1feTd1~ z0#NplmNbYcVpuQH7doOMK|jzSuhP`hY)1e*LLT3;Bt5_Jp|>Z?PC;1gxcLNH&70j2 zd2W{0JIzHTw~Y3x&wENkO5;1CSLnF@!SP&W_{QI&D&p*BM=vzr^PvNzt0S25`=Tm! zLZr6!;;ROLYsK%?^w94M-z{Fcep<@CRC_`5Nj4r!fB<(Yqirx|>PmBfrkig5iLShO z*8DC=QV_+&f#fJ4oRG3r*T}*1zn>v8s{i_>GmP{=`nnb^AonJ75#&?eA?|F|(Knf; z*`EF|O@PxJm6j~
      Vh6zOYi*22Y1;VKywQmSKQ9!(W6Zs96)r=uu_#NaF=#n>Q~ zni>;XAP23hj1FlTW{&h!oHgJ3ch4U`x%pMNdl#=*_b%@p$tbiU4(bjvp@3w7RS-=$ z^xCCkD$2pf@T}MI%q3WdMuUjzd2^uUwP(leH=a}P;W(~;JhZ{*xIVHEHU&@z4R?^p zl4BQ9jcI(3a+9z7*R9GF31s@$otD$MpveEdY4r$2i3zrEFAN1kfwC! z{g~jz@xA(P)3oFgfA{B^Q)sN<>2a>d9>>upD0nn zO8Gwg?@aZ-Pl|_19Lik5u`cTQZIgTMFLZb-Iug9o5GG34;tC%-74A0n_4Q@z!S^96 zyI?|VTJKwbf4^LZU&Wi6=SeMxlyu9R1J@a>&sDUZu-2|8&1Y9?!DtOIWxx>^l{^)C zr{>qO5=-|J)4`%Etlm(byafSGV*T^b(27S=Iqhg4sG#tf7`)l;xlQ}9i!sTHo%SJ; zJ&f?je28$iGYA=Dyx2$mFg#)R870rnWnr#R&rmXIM+;Su)?v47Ma7`fj_+mZ=;%;9bjXDO3&+uYo1IvUkL^8N z&nV6tzp_{8rr0EoSO0&|vO$QhS6lQt3AJorK7%Xj()?D_%s@y^ZS z%*>EV^emWtpe`SIKOdL~J|{n@u(-7q54h>Rd6=Cz>|lKd)vi1@ZnlA59?*$X8# zj}}NmF+0t>3w6OlSk zQZ|jh2miQfpo$nYHueM(2A_vu>0m&{=T)>6{2#9BLMkFDFsMZzlHF}&fP=Q%cJ_R; zzzNzxkl6Zx217h4G8C|}@;!xg8zJn$X8^EQo)(8-{KSK)nsfc~kfTx49cr#dc}GGB zAILoQ`TQ?k>y)n0$tR8*(L6l zxLqPBUiiAfkO(j2Lc{8L?AlBcVs#uoAdRC#jK%_lgp}kuAIX>e=CBizd!QKS)Ev!B zi#(x`$0>jR$2U29rM>3CtguJd)9yt@bt^Yc3J?OguzM+7s8%HupNun~DN^o~ILsnb z>nNZDisU$@oC%9DsSF7|?wWZJh?&M4^6AV!W61;NWB~Br-v@m>Xx;%)UBwTlV`zvJ zMgmgMGOs*4d_BMUo%{9%rUm9dS{y?DyadY!fO`v|Dpj;-pN}sSy1)t~>oC@TLJ+DI zh$SuM!CL~~qn%j^#-)C2#JbZ;N_9#~+yZPuL`|w|2piXplNXRt${QqQk|h}O3(!<5 z1I6@I?$I&QZM0eBc=9A zN%3CLB>BvYJ%LAh9KYw(y^tFa`mmar7RV;tL!n;;tsN;nS7mjWcW- znDZc$2}Jj}Hd@TrykWltUZ}?Nfs{aP7&n12Q0O2c1+u;D2k;sqUZF^ege@M2!I{*i zs%`exfI-TIw`mUE-!u@>!I}gbysD6u6q^!6I8vbu(f5` z=YX-zyu7?Jp`_nfYrW1<>+^)bzp9O6Zn{2Wocq*7Tx_@=O@}T2yxWJ=J{RwIEx=$$ za#Z@h6_VwloYQt58x8E?J$5~WcQ_C=jLaXG45f0Pa|F~AyoD$YLZX|i!E7xGo6jMy%?Vqt#1Hxf=W zik)3w5t2q2i4Ty7k7Ngr!UO4p53%~%rBbMw!NjB9uy>sKmD#G%91Edo$6gT6R=q^sI6+)}@}pzzl6PVW3+b zc(C2d`mDnhVE0F9?kC{whiQ?5U6QfIYYXYx<@fiIPeV>KqoeuA>!Bz@G zYkIY{r8Ylvn9~H2Xi-)E8=0p9XEIMs?Tj-7)wS=C(+Vf^2{ zRa#Y+vb*~8uY1YFw$S&NEhwK6zL2d`ub)Ah8e=!C^aA7%!GP+J{uE#1ZkW8PcK z-l92{GFyKB{tIj%$6m{N54^j22@J=dDTQ-2ez9>~_ljOfi#UYDPt;D>-{|N7np?Zt z8E~ZVF5dD9a)SWMCxw75E(b?9oYWI|-Q>F3&S7NO<9}huJG`ywX@Tm)2;&8!M3N!0 z5Q*wdu3|>Ce9yZO%l_DqhAH*Tp2vY_VS6)*oFjXDOA1D<@EUmhalc1DbTXW5K4NLg z+yeX=LMjN&38_#nYNSr`vl0+h25L-+gQO$?%3expoAr#2+hjW%)%)3BAc%>hP(4$- ziS7nRg;JYfOfMJ>K?c$RK}HwQfiQw_z85NK@8QqXgM?;Sb-hJHy(Z*Zh-3ny??Le{ z!l39X#a*{?c9S*>V@vQbK}p<(X?zVKTkbfg01#hO>fi6pJ5azNjwUA03-c3h>|q@a z9D@-E@kTC7Rz6e3m{G>hw3wOBV=!3-MiC)7@P{w~ zK+0<=RN7;#p^6J+FZveQWI5*|(eDdL1J+ zLc9jvE$M3h+`nIYZG9m(Y!TgF9~mbSn%Z$kWMchqp_KIWMi}9W9~0%V*GiAH#2wddHi3w(*1&w52@ zgX)7rpS&oHY*{{fJAYY|FU6;y_R%C67G-7&456?|Ho<2O~*Hkyj zU&Ki09}UPIrEn=xCSU8j5R&_Z*qIqwWw5AL*M_GDI(;Mr%>RR-iXmb(r9j6)-BD_! z=oiQi$|Ojp1s*~NK&lpPH3SnobS)r)SYY~C_v)*LCpY1Yr);gJEZj~BjX6*Us&zt%DS-CDRQ#Esf$(Av2rN#FjkoHg(rfw&p6*Jke<}(Xjcb{lV)f zTHuUR>{C%`l@rrQGZ zKa&Iy$HdR<34(JDJ(3nnkAw^e& zjK=Juh|9ZbnP%?FY|NdTtBYz*U|nH+t#|SQPelkva*r=xAjBQwEmP`t>gvKJWEtR{ zsI)XIvyz9Zb&1aQ7u*s^&in+$itAMnIIsx;E>B-JX8v{7zQ!$fMzYdOOge_E?IFrM z%~C9VOiao~mYMHe%fpxqhO4|snMmRsj0i~gN?8`jqUdC^1JLQP-8qE#h5q=X;znPe z=-qR5=F{ih`6ny14pq-F>HSI9YYST$X!%bQ!b-!ez$(%g-<;#wN*!%4dnlA7XHf{( zaTJuHw-HR;(xOAtb&(Rt12uUsg|ofj{;AK0agn$#3aC_oClbY685nGOauY^zplJxA z@c3>Pq@7=NEeZ4%>o!#m{O-qY!T zZW!E*f*#k4+S)GQO2~&I8UwY*p>uHVT|ffxrV>9FT+M+D399!&#F4NCN=rE0?V6}v zXq8O4vcO}C(537qH!`Ae1i$uq!~PHR_tj>#&72%H@0!)=C#7Bxp=1Tz3mXms4XZke zb)VE(t;>#Bq+qoJ#<{vtOV!0+OG%x@mhO9iI0H1to*^0*lD|t4qQnvp!SQ2;s?dMj zmW}@dRd4{1>H|#Q4tE<{TS`1)T0Z)VdF^1a{?1P#-S15FhiK1CUWs(`MODc@*(j)V z^pm|lN{bv;2dkUx-})HxiAJjHe);es$_p_2yMAELay8uF=%_W01G;fegz= z!@8JAiDR3zj8HTxHRszEPcOgu5hJMOM#r59yN#lx9K!#&_2;?ghwX*F6R_*oyqP@D zr;17nyuvTVf)Ve5FeJ-MF~kX*ElT+!7!8P#N;XC8OmG^na5p-QH%g^{s}z$o{7Az5i}BWNM|%1HJ9S6s?rLbPuYxr)`?sMaI+twX6=B!>p{ohTm@@gGYx*F{I18#m!S#! zcv+#iM*z$+u;-N{F5W5+$Sc0YyUiYg?*GR(BQ}}d_M%6^$cCep*^c2aMyncBOTXGz zkR0DUAYLF{a5d#$$+`Hfxql_kq(nYC%X4P#-{E6JT&rZQ9lJ^=Gb3Fh{T~9GGEV4Z zMrLM!XoFGq_Kyr~Tv%=a!Vo{}Gp%eiVeI*VZ^@+2+h@}G%3PVuOmCRAA^3Ojc7$1eTmfF9HuzANe)Pk*x8X-a>@uK zCJ-Cqta}eb!#}CYe|W@b^H1&SzW)Q!sR6kE`F%C%`w;wQ0VX`&*Hu-pTfqdmn|o4_ zaO0F!S0W@y0^jfXHU;h*WnTS}%)nia@G zY-45$4oDbrJ*JEBOp$*gC9cVLRIdJ_TkO)3KO6zXv-kBCK`&)%k@H~;{b~<~t%R@B z)g{K~&{5aY+RB1^nlQNw*DFtz*0vYP9zt5LwoCxhR9u#+8A7TC7sPLi8I<7)hTys= z!d+b(g%hh^bG(1|M!f&yi7?SC7G6Y!IRwH>U#HA&b_` zf#l~)elO*x#xWRB{YMpAp~z4N0@7`6MbjI$s9`kYTJ|w1TKg3>C(h%$x^e#)2ksXM z#982eqMyaRMF@9~GuD z0?f^Wp=~EbY)oY4=3mL~es|VoyzOcKOXrs^X_BvHEVAVu?#-ak8?XT?_;P zFA@wz0d$d0%;x~k*F9@PI}Xi7mE?~Dy=px7JR1x?wJTzC0q(7=Gs1n@#r85+ZIKgM z(vaa?j3*t>>ahE~yG1!sl8SCo5`tHyWaYtnC7SY}3|RErfokc2Yyf6@BTPJ@Kc1B+ihk2K_b*Ooo$-QuaYh#T@$?;eTi(vjEp5tJn?y$ZG)c8lRdu~uAXi#Y?{hmuBbXnWcCxeYo3)~lu0fE9ReL$mKb6WVW?qEV6M&igIyTt?ihzG72Rb*tAwY4F!yQSX+P>SKQDjk?E`9kWjTG? zhYob+R2~YNF|x?{`liV9%-xF@CC-@PQOB?=sw+W$-Y}4zUjpr&;ys`IZx#%#)y%G~ zuD0kQjAXK{Fo5NusU1P_-uTxW*}z~o`!eXj25L7CU~WjqX2!5NbcyTB@QUe+MWNF4 z#W;3lb0H^D1@VUs6q~*6NBdG$Vhr zA3?(1kgqQ$Lz%XKzs!SNWe&4!w~ScVABu|mHQc2d%UU%cj#7X>?hmdY zztIBA&$aq@`TU#j>w*O1gDMjuIwtm6u5M)gzaB`nNK3O}Jv5mWrd2o?24jWWi(9fF1NR=CddLwrg8E`MsqXB=w)V zo^zq=d`8MnkCispGxb^qrttU=r)qxhfnXt)G!J9iu%?r6KUs$Hdd9~~-`@0w|1F95 ztB}9BSn_90(I3&;E$hn=iwz~n-U|#M5?5-WWA=AzeIjCWLbUeU%-2;YHiF45)pPax z0yhmsU9Vo<(_ztH>!-K%!%%djk0&%T7YiIn{tDST{TLmC%Rs+6iy(3a@ex%lV~LjgoMtn6%|rT+^qpw1cv9 zydnWY>|&8ao~#a7l9i88<9*i$s?1zmycvW@XJc8&WI4E)T^_t(Qdv7AU*l|X^6cKU zV_?ovgu>j81V@kJSYCNjM(SeYh=*U}5-lV6R8Ho)b5Jkq2QLxD zW2O{|hMyakJaqG)kknMm1!z>H!Je|-n&JapXULL}j>E^Cu8H-w{B@4n&7ZYYWWLOq z08LC}c1!QAmBodVy{C^y8}Mdx?J0P{)N#YDvCO&e1(($E(@}1QeaNx0zu1>j`jknvrwXOM;fKd;I|_|Q^vHx^2ZgRfm9V4Nrgf< zS|n$_KCUs#tW<%lSj*E?x%~uzv3~jeSy)@+zE(J{|UChwkj=Jr#NZ`=@Jbz00v0CaqVHW^Qc_e8e*dpVvUP6MPDWbxZEmG>~2&zIh>1 z>IgM&w05-)n<&bU1rPC41Nn6c?pX4r=BJ3S4R$zzU!#z3!VST3S{tjbs6LKi+JL|)v|U62kyu1=sr4xepbaMoN`g8 z;OYuE3TEx<4;~6a{b%n#%iUY@IM=dEHsfTpfI3?u(GOBWeKWHX*9WiCSX>@7O$xBV zxIlHfa{ynNQu$ZQXW(t6#4#j9jBRJshD_<=x1}H-Oha5mYD`&bMg~>_3PCEL2Ii_M zhpl-k9~)l+@zS@we1S6-4nZ6e4GEEAlNEc8FltM%H{2FXk?Yq9n)w9R@vUY@MYq&} z=0}mqjNUf~Oa=o-chWm1$d%$jh-S4#keg}6S36a6{oLRRHJ_Diw0Eac&911rNod<} zDR9xp7j(>o_8G{gHu3S*k<5r`{`qp&N_m!}S8i)Lquz?i~?T6g8@deUp#jaPLvL*baFo2lwj39l`zbv}d~ zIyVGCRLjKX(yULZ&4Ij(TazbkYci~PQW+TW)(IguNi&>TI=_w4ZCONMaH)8>CQ zhE%?nM8oy)p{Pt`z9w`U?);`~#_d;ozV#V?K}~)&6s>0XwNEf1roFK4f37+p0gjZS z!R62&vHOC4j~Kx{=|`ije*Ef_A%ESv{3=~5&wV^}r+?tPyO7D>@HQpx#MdgH6EyLc z^7u;w;3hA{Qq2r-j=SlyaD&=mS zS%6zG*H;o_%lZCA1LIpzLdQ71?DA6F)o6@6O##mO1` zgns8F5#FZlamHP)2zNjvjnA0D@4jfHkp>fZ5Tc+%1^^!wM;5uMz_9B)MSlr7^(+ZBPDW%{)< zd|qQ>^wiD949>ShY^@8-`H=6Zuj7%X<(pFd2O|Tc$QpB#DexLHyMMfo8njU+M#AR? zXbGu%_QCBBfRjk3E_cuhEz8!dZ(16f1a#s{3>-=fM1q4OJ0GPU7|1uckB*S9mn34) z+;q6cl>dz(;KaghBM(0tQ!D^y;(kH4(NrjTj8ME@-}Z{EUUW0F3USD{xo_hy53Q45 zpnfb9Q)=qPz54J<2Vw}oj*hStcr9J336S!4o1Li_H~mh>{+n^Y--Lt&CQd&-@SMCV zM_eF4u=!|$8GJL0xBgN@UtT+7=Q(HSxmM;4@=5!o4;cR-DmMtw53IU7!0X}(>thq3 ziX!)U6FwW3*^TXvA*2k6YQ_T~$5ESU=(D%D=QUdGwY6<%SM?`zpLwo)s|~fn^s8Ri z$JRMkFYP$C7Tpq!;BiiQY@Ks0k#IVf&?*D1Dp}9Epf}%K(^?O6c5QfcW-_VbKJC{1 zPVx{OZKoW|_KqY2L(9?*8v_7u6!lh9Dy89# z<^Y_eiCW14CDy_TQan#r+Ekifq;16Kag>c{NYa-Z17B9||9CCfaMI!eWt{5;wYPmS zO2o*mKsNDallh;*9byJ*qVXw58(43bCATJ+Lm+N(uPuxWo(z7{{zM@NKJgin)yB8e za%xINaNb7=#F1o@=UPeTbidC|KU{hfAgTdZo*&GlV`~4f_y2f*;C8=7+4sNPbE)u6 zk7V^-we0#VRH{DU8&o$E--=hDn$c2X`t-@!&$iFHAph!v0nZTIg_2IMW}Mo75Sa7b z?su3U5JAvCvS|llpwaY7Uz?Tyae^YicA;IP?^?P{XV)4#h$zt$f$9zA^ZVC4*Z~lL zL7oLJL`LtW02ukfhzQ0m2ndvUsr}dJ=)=FOKP=-1S|5p^1?BTuBIJ!ZspuS{ZE$R= z3M~rH;cN;JuIQLuT`#f>-@obO^p7YUL+)dyxFNQzqg`^|en_0Htqnv(6?*?V+by@w zjpGK2sA@fCacy_KDth>QRg2Z$rd2t>QQdNT7s-0n8^7B*JifCc3HY+wfTboTj_H3< zR(>7wNHHHW{-cc3m-XJ3f*S^rP`U{dlFgf$JINr&`5R#T-{CbIE~MOVM>01M<)1pq z0tsBP<5Tk57zK2azxEVAPlIOK=`of4%f373&flCn?%)6P->F$$8@Ag^2IYgD7Xmk5 zr&R%&0WvTm7q$GEH^XE3>b3OCLAH078a>W0eFiXLa>JpS@M5Y(m|if%FTlIiQ6KL6 zRa#mOL66|+S}EjD@1;D>4I>CRr#vU?%(WD8ocJkUkeIL5iLd`2S~2?6WAyeqUlXnL zv@X$>G$?v+|JLt-Yu>Ung_uYv`O=XD2fs9h{G~$~iGjW$YfGMz|IX?X3dnzSAq)x% z#rv#ny_&)aR<&0c*-KrPkPVG>3 z^yM~W0P6%x#=BmXyMO1lo9(t~V1alBm_JA`0EO|5vrRZG3N|p?+!uOd_`u_yPelW* zvCTr9l$e#l9F`~~>ZELG#ZhWuSlX&#Q}X?o((yp*VO%T0(i9#wU{k2nK#bTd8A1 z1GSd-ue0Z5{sjSOx@PR}?v0Q8B8$4fMeDSL!0D=<75VjbF#Wv{=A8i*iH!9Ngg1j5 z^;As({qMF;f!`Nn>2Sq-1{kC~`Ij4zwDAC;e1^G*$I*Q6_@UxD*&6P-ymDTQ{!-cf zQLGrGqQ^(+xS8OUjofP6wX{CIPMk+t`+e zPqYKaX7Q;lk!u+j!*B$m>Qi#*;qCs~Tc7t1Zc~n_9Nf9HS$I|RYE*`%IV-C#U7|vy-NJ z37zmibIkeDfpcoRXZ=p6gir02S!YIyF2;IJfKK9tgokR}3qCK4fo#VklMLDdg6|KA zP_|w&we>qz%!RG>r4|k}kV0!HjSElIrqj_f9AS7b{P1H?YQ_DZGb_WRPq%s=H~I01 z;!QV5mwJajtfNi;k{`(9f3s z%+e#Smq}cHQfsQ>?a^k3v_CHE(S0erB_dzK2&dU>f6k1)o}bpuE9FrBwKzIDG+b43 zDe?v>m|9f|BroOu+>mPuk=r(zS&=3}&00Wi_JVvqgQS(s<9m~Liu45$8q;id=4|he zp2|OcocC+U>ZH!cb~YSRZDqIHt~fjiELPM$_^C-e)1`g0c{|JS0d2`^!z4^V2ku#i zAkF*Dw8+lGSSkaqGZ?)m(@J5rl1=qz%UOp5sgIkbnogZ*d0IYh`siR%Y@qGD|Lrr! z-Z+Sq#x{CNnjPajhAz<;eB3>&s^wIlqxDqlR;cMbSG{UI172k8{pRV)ojy)PG4$(7 z#`?c1?DgFD_YA|HKh*C`A~im22L8)HxBH9r1vh%qIlp~|YWfWC8AB zP5vO$S1bZgi{R7}Z`K4`L~&edTU)lhcK?)$WNERcLtWEAy(D~BKnP*v9Ghvsvx(XA z*g|9y5bDU(pMJNEiJps|e@z}~@;4uYve*`9#;)F?j6sS{oC*7|YQ1D!Q6%uD*wxS` zhguOCjj7Yug(K7*@vm>at4HW}aPVR2`x^)V=BrI|ve!<RM(f#GYiBcvpuODp1wHLH|Sr{wB%1ym$2 zY#x$ZZP>2uL9Q%Tq6M@aNFVa-j*zte8pt4gT))&l9T!lYcSq>NhuC{Bdu6oq@SFne zkgFmhrsO$KL;2j^T2fvG%c;chY?W&5nNfy^au%>FeVYjnyANISm9t_*qh}IrQmp^ z)BN4CBBO~Z=lOtApzc9!I0*yIrS(@FV*AHK*-u;M2!axffpEBjQeBRgE*u)(&@-y_ z=yzH;HU%m*aa`7Ok@&kpIc?Tl@~Yx$BmIPIa_;W9v6I_JyV9hT+&1z5a`_#WM?ZHi zXcSBfN7}yg5gtRrkG{cF>iZKu$Y$Fc(R)(+91Re*{lEAFtYkah=lAsuL>3ALDXt6e zW?jd&zJ5T2YFKHip?`OLlIo(^VP*F+!AK`o_fh!Mgc;$9nCvrn?x*br{gI=lk{vVQ z0r$UhCQKgeEOi@6t`BUhV?!fU5?hYsp|NZIL&UZU~_vez;OQUSGZ|UM=ZSnl&33@ij zUfF)6u~sl?BeA4&uoOZK{{*=g4nh3gz}MwGXK63R^Pd%REWQQNnbT5=L?!=$VGy5{ zEaitF!@-|&)pfFunZwFreBR02;m1z9Bhv7hDj$O=vavBsom6m`*jRJ;IR`9WWz%@3}{2MYQ1O@!ladY zIM2#xYv(lktIS~S5~5Orun=1{PWo&+l$Mnl#5WR%wl_Y<$2B7wt^J4^i%;qzbr6C9 zY<(@+v@_<87&?6Jp_0Q_JT*szB9HU3y zl!I0%&F8$_TC!a8X31=eMzD#oG(CL-5_XHpt2>mm|C%J)`BU>Hw}ha2)D>{fkANal z(177myGd3Q^^~~Cv@X9lv6}4sr$zBY_0P@iVNJojzFZqyncmzE{t`>ef}VsB!nK}+ z8`li-F6z!DNc8^{H^0^^^Q9x7WI)suC3beCoSPHBMl$b|`C|~@hLYH7LpeX~qZ1{@ zX`Qz2%t*7&iT%UswKGmtdc4^Fx}CeyRpz=UzFbn@#(UT3u(c?Z6usnx@i{6DCQ2nH znh|Dg(f11vJEne_mW5^|V24wr&j_6rN;uPj*XDns6Ub(G?OITFNf4)!h%sKIDVK%k z#Be#wQx=>F5RtA(1{O7?oGW`~cC{(s8DBA$AxyDA!Z8J3Q&uAp)gxGPkAv14p7|mB z%7xUbd~&HpO+=a9VuncdA zL-eK*D%HN0uG?>%&l0UqZQVz)F@3?~hN)#%E1O3BSGz1@OAN#-2KW?m;PFrowF60s zS{?p_f5flf642O#LP4P? z9P6~tG$mjlCmLS-ouMM-vc>^jipk|nKwtr%fCdcb@M}l{(c_1`*MFf3EZWkIo?*K; zWbzm6|Nd(2M_70SIV?wyC+d7NMSdPVeoF(=BJ%w3 zQzzm!q{c^(5u2M5_QMv>&ytEc(Eg^NGg9-b`xe6mJJ0y0rwvVMUh{&f?5QmK|M>vR z@p%P)=oil9EP*)F@|VKf)6I|$LH-kA3cCIx78aMeB(LB0JSB8s5CLprBW(A{nhL%E zd(?|6u=3FH;CSfN(SCsdinAMVj9u<}S29};a*H}PFtv5toKA+Ugh<1INDUVHJgnGk zTt@tZRlA*n8s{B^SlE>(xAUWUy=WZCkkxBtduwBt_uvNGX@VV^9jijnlR!2A++p67 z3?%Pk;Rlid@D5MT_y&^$Xa|0Zo06WJ6n?mTapCM zi5c>pWBigtf?oi1H~2dPfQ-1vjwAtd9kF!4_B^*mw!?FYyW(tu$qhEF!BHAt2(22T0#O4lN_R?13ADe) z-Z{M;8yjP@&RJM2c?duiGT}=Baj4&mNCmPDlTSp)8=>DFzvCGTYeJuWeRe8p+85Gg zAA~o(9T=%(zs(`l@u>Fk(fILO{E}2Q9r%)S8e;cZBMk1{?mu_o!i5j{n-4jIJ55+6 zTj%RqtoL*vv?<8yeer$qz$!Vx@4 zNSVb=$ITYm#|B7%U}-7E4Pwrm41#VzyQXRiI{t75njPSChqjKn&PH+yI|JLH`taRIO4`0MZB%@C+`1`yN4yrLf=2>BplmrdO>s z58nl%;BPg4;^JcTnK2>I{Do~r*`1orNj93y+?&~JMWi|z zcRn&Yz`JO)*>=-)qUpHQPRqT8_&}sp^HSgdQaS*AfU$-i_1j?J?u!6~xZkd_i#S%^ z{HyBrUjm{HhA0mrLbnE6>^@U=ONek7d=KU`B5Ij70=3=LK8^nu8GoX8Kgqc-BDzz1 zE}(k7M}#kORV5`(-{7NL_38%UndA(wq|-u|>p*yPU}V6URRyBuyyud1#tvf!avSxv z=?k=*0Fd{Fg9gxyx+~oPZ>(SPs(H5P5$x~`2_48<`T|o@I&-R>IHJXLmZ4xZ89w$C z;rB1(SX{f&&d**z?nsizyT!eD&n-0Z7SSBR_TJ)d`d;j{w(RRrR_L@L1FR8=fExye zokP#b4%g=d4qIm{Fh=474kq>~npsdafL>tnVTlWMhX6%p*e@2EC7Y!V zCdt=hSg!nLh`*+~5Zj{hC?e~de)KhLyWkoxuWb++Fybe79+C{=a713Q)((E`LSM&w zg_@e0ftr#%tvj!N&|>`w0;m3d4B0>w{s2RUj;w;*S8m6lvQLWy*|T6kzl=PWuV%Ra z2I2F4)y#v;V(=M? z#{`B$G1T~|@d=HgO4;}IwQ{#9nm>b9fH44u@qeL!=#T^O0Xpazu9I}I>1!;R3L6D# z*V5hfBbx;+^IH`#3J3xXaq#Kj{ucl|0TAJA`rEEI^ObB3ihgKuE^<3Wfc_NI`` z0TwrJ&yPf6hFvuN9}s9M*`mxRQSv3fpf5%D`xEVZy$5@hbFSFx>RD%hcDIJGp_2K# zD?Q#~T5MdfDE$_~1__)oQ)XBI@CzV#SX=Ohj&s%#l$i2C8lZ@deK@{iA*$gVha>Co zLE2IWeG|wafr_<)V$oM!d5iX;bEL_&F+0Q)6QdNHC2==Ww}E8|73tA?_gw7305C)1RLrr=VK`b}6|h4H>04YMO&^s#9sFeRr#B<{%^I@y zSRCyY(b=ilM<7-j0@j{;ZxYtuMfc`2cf_#%r#53xO~5^h#cl-L-1WTxv(n2B{=m^i zUmrCUF*Rcnuxx6c(UNgMg-RIEeDbwq%Ccc;FPAzefzu;s3|eDKE%aRf(J zDX`lgAdkU(M2Y7paF~bK_Al5=8#{!=W_3K0LqbWK1ARoHCMU2!}QFGwhG{)xzjz}F{O)A5IU)?VxIUwZH-koXV(``IYWv*}b1-Ki1dY zE&n~d+rW6~I_ZFdsxR{LWh+i7om9NPhr9#t2SBT*feCSO^IQC_8i(|`vGOjD^OeVk z^Xa$)^ZCLOn`Lp!3=Yu*qUqJpty~$A>BiBWitVmN@2Kt?WKLD1TvPVp{#Z%rc*eYZIo2+RKquU&teAC(av5I<&O3khB0nl%|KABc*4}`GTyI zhcBS}Homkl(^uSoADDwf1Mz zmkz)A^6X28UWue=i6zWYJ5zcg&)i^7R4!PB2tuZ*Ig)2rd!Zsz;4Wn2@(VPiW()H}u@MI`DKKVI;%v^2qe*`A3_7E? zduyZFJKjDxHETT>5{t=x7}W@Va>~?hz3X93zg-`JE{n=*Y^C+OcU)eE`y=xI#;bj7 zg*Ce}Ae&m#2r_JL%T32OL0+pZ;mFPEzyIwnCCZsQ*n~r@Vc*YmDfT-b=5Eh7@THH_ zU%{!M=?;AgJOFb>wgL%xE<=T5Nd$pT#Zoj0B}jSL1Q&z)-vAQ;XB>|5Fk1pWftXKD z`k3VqeU9z$uzqfWzb&c*FHbuqT%>m^rYQv{FRuu4!5J_m?|*G%!KwmL19UD>&jFDv ze_-_|je+7>qvY*=L|{3G1@OUyV8T(}6k9;l;o8>&JiA&ENkIsVj+9<-f)y}t`SjjW zDVSb`StwL*T<}5$ctNNl)qEe@S>E!E-Ehxcm&=Gd)98%<1-qKo!ID{|z6)K%hPiBw zGhS?;p4|=Y-dS6;|25zmnf)%oYWr(vbjom9QJ>0$s?y1Vgg-??K}-N^m|qS;@#GgP>Y zXfkBW{mn+EM|X&kgt_I=4Lja*zI!{@5%s2xON(yTr#w`_!-&!I&W5bd>>|h3T3OA` zsq@M0d#ix;$stB}Jy2nfymyBAbzM;SaksU1R_~S?k31Mn>o9-La~ri-v*B&zK5@15 zlzS)vDlyT%*lB>!BLqWu+pDbx&jaiSakINi35A^Se&|GLK;S%!0?kW~>XHGe-<^@! zh8NU37WfKYn;W*mdL12b0Uh;6e$z^)QWZeF!nNTOl`L&Q-x&u06 zJ5@W`WWD~K3^D}3<-ebze!TOWCvfQYbDE9*7(ohV5*a{N|=EuNJH>axI?z;iK7R1Vhz zbPmGRXgu2}>GIVZaP5Bq0{LZWzNGti^xm?ZB>-n1PWd3Q1l{<6$I0o{))2CbOZ1W9 z4_6eZxchLR39&y3f@C8F=|vCM$X(zYNC6SEpMYF1gek(*XdxDbhC^O(H)v(H%js{& ziG}$EM~6AqLZe-?u1ufvrdKB~t8E*^l`m$W+BuVa#|@YPLco2>MpT)*ciVkhr{#9d z5MUo-JJf@41QW;R6Kv5sSy1-X`oewh60bkRn=5}i*?$fF=`Y_N`fuICi(+!`CZnMK z#|A1$2<7$@R0ur;$u`JL5a!R(Uh`%gm)sNJ9U}IKrwit6|IukH|6M~-xN->xIfk}+ zb_W>RzbB!O*=vOzIqx-yl~a|lbkN+@6@{?B1&>`^pA5g~_CxI&G|&B=FQ(#T^sxTN zFRxzg-l;8t+SIxLm^e|?4Z<+4rEm9}E`pW@Y5NzYrKHBQs`#wj zi{gPHvj{+BFecTFJ%$ z6BNd{wIM8vB1b{0!6kU()j+HFmoUAm+@Vq&OBy}?fp zE_)n`z)gi!&@=;KA;=aO44Roa4lNdM4#V(Yp2e}jiVh)6s;c{uE039EphE?7Q({7} z{`Pst<&}!g`)8PQ!3O*%@DxzC##q+b3um1^VM!IHI4O?;v$*@W#!So}Z{~qln!OY* zN2y2rSpryDWNy97oI=QhfHC`Y!_*t!gSc4qh=Zo;^3qy^=i=CI_;)29nWF`Hh+V771$=N&-LrSXwME(pA!)i~l{T|;1E0X-n|Fe3a^MN3)D z=D9W-<>h1RQ3*7#^LCzljV@4X^HRy@UoGhgn7I#>A$PV5A>1#!ZT_BS{I#&a0Ab3XQv_QZA2MCj)T8CD85w3j zyha*r?id)k*esR_1JAAR+2yh@t+KfN#>Vj2ueefM{_uKeW{UNNQ@cssmK`%r$HJ3J zZ);WHZ^~NtYgY)3WOKfrPE|Q2wbnM&+-#&Ic&R>_99)Q+IQjJuw*FE*k4!Y$g|Xg) zas$8N_uRGo$QKV-A3m~ha^d8cOD}I0e#v$};8M7tJ(}Ie_JybwrbCpE)FEooJ!e5% z2N9^zdZM8TzP-Yvw7excV+xXsxx2KLYCg_A`=aUFSjueopf)_VIF1Y1yyAWw3t~Px zq8$+lI^>BZJ^VdVoJo?ZrM@Vyx^awPYh0`#qWn`{XeOz_Z^FtCaxDh+Yz+vWrG_rP zPUWXhC$Df2nVQV-# zl$v&?nh7 z=7~mZ6|*CmErU-OA9l0tOud2;i2Du{+n7-F5#zqQ2Lkm)!l+HJDz$XT*K0sHX$n?I z#9u+FLYvfzfL>gRrkPAY$^;(f_XB<|49r|;J^w`G%s`5GZn2I)14TSFPCGdftpc`M z+^uLThjH*2_?8BY{$~k5YTgf^1Pmzpr*9IR9rcsvg1-+Il(m%5OjK&VNcV*3(I|pkTZa7I18Us-xC5B=xW*^S2VHK)V`fkO zW#Z>W9On9dOx>zlJA2CG8#V;DY!p}CZ*SWo)_t)76eP zU-Z{85j|JA@f*rfvCPx69gwMz`5>^_Jtq#`K`dJpVb<+v3M$DfO>Ic06b%S!b?0@z zM%a5=zcGuryL~DBwJ(~q3iGFi{i3kjsN$imFi8V6@xG~tAX+$i($@nv*iUZY%RQt{ zGI*i$>H9?a|h$e*pLZ`Oc_{cQ`Djg2d zz!GeLaT7QFMSDFg7e}G*3l7@4*yWYwX8*8x__p+1FCHHTOrI4f zu^fTE>CRkNh4oitt{b+i=|mK35z$pd!VeKlCl*#AG~g1^DUd;qkW``X4;B|?Y7q=3 z62FQc#t8g`4Nn~QmO=}?8eH#xC>CW}_1-e#0(-@eDbZ&a@P#RgDssd`WS~^h+{MW1 zGMf8{%Yg?o@*l&Ec_}C<(I`IdKn+M8)@=DAeZh*rd-Wr6gHnmV@tsV$z% z6|B?$FT=nJ3@~XoT2wCABMTiOncZ$sDXH=!cMNjyPq{B~!>+QF$4tH+60KXfPx0mI zp+_knq1A%*VQ+MQfjUrrgf}cxNx)Z+gI_w;P$j+U7*Ohd2T-(C!>xIwf4rnXS>TVPI? zT6_}H7d!?9d0VCM`JmyF;r)8?!EKfP|K7iLl91%&9IU)NH1z)REyGlISLa2JZ@x#E zeLp-Zq*%D(8c4msd?nC>shTQqk*AW@Zz?LJQ1{}ddE^U-%8tA+;tcsI zfiJki`yAEl%eHDA^hoiro~Rm5FV4|lG<}zTDK691a?Q2{qBesK+fOPGA<0JWVY3+?QLvH>I!b495JQ&H(dlnL zdeSa>rSPz;#GK%Rg||oKI$=RlF*XplcJCBbF$M&J-oL%dKjSLa{LOqFu2=6>=c+}sOj zbU1~$jOgU*V9%|0o}|J2amk6bZAq1A4vWlz(~Z}$;5amKB>8e(jqCUG<-b3-14$L~ z?GMe|8xQl$K)Z^?9os{}$+V3+@zhr;tD`zA*(v9vx_RSPOPH*v^p(~im|^|DZy$i8 zeP9sR23{?Q#5xpakVp*hKjKI`%EP656rLfB<^NL1zO@FqxOh2+kcJ1va3NlNy;xUEv4ZV&d%%h|^_n!3F*Ks5wdk$w0I8-r5y zAq#Z_0FjR+Ts66T9*GanE>97w;=2Yy$w+iKPcZ*#zWY{eNamrJmzkMhNvV?y)jF5| zchdt)`rP%D(MRiL1D3c(8ZH51R`20j?y+&O+8v;Lp!~!T`I7k0n829tgU4Op7 zfHHc5(e)=}YXuMj5_O0a+RtOv+QMYx9P$1`!u;J7sWGjUJSC!S=6V<{aI5H>2~CI~ z2z*Iz#9xaD=6Q*nVV7VUe%{~)jc)63SzL)~!$g7kixP7@M9O1Q94}O6^d(B7@w^R! zep?@louuNf+jIm0FI%M`*P3>GqnpO?!Tajv&LDh3q? zvJAPvc;w-F(|lh+ye(;(a5e;sfLESE^8nF1l}@;q4~ZSnPs!e<=@QK z1&BBRf;Z&eL)2nO<*x8ikGN`1FyBLVZV>Uwaz$otZu7}1+_E=PQhn!dtU$xILoMzd z&N~84RV;do-O+PSil4X5rnz~BrlP{GaVAL9A-XHwKJRpx8_8+v5Bn`2+YrEsVerzn z;7YFxvO@@YZxzoUBOL>Su7kP@v2JDblqW>%DxA$Z?2Rc{UA{GPk}7ntdGss~n`L_m z?*L99>%kkn0YDm(BClWi!vf!llgs$@g_na*aaM@3uNqc4uXzBT18Rkjq8`2z$b-LyA4DnStFyCKy7uU zaZ2ukFz-)CwSq*80>PmJzn)rrDoqfzGCI~VD+VUBmSQtGMYn|ZY z`crY|gZajLbhzftx=0Tv7iIxW&Y?Ko8q z)gm&v3El`}SLf$PF*tUeVW@K|Me1KoEx0pRu>J3k92g5Rw&KG*=86L{h= zuZ}1Q0;E^`O|x!zVO~N)oJk+JfrI$vcbJ!lCCXz^kCo1&aSWD>r-0#XQi~91&x&T^t*kIiFd9$Vs1Zu&|@F??$0IU&u zL%W=5=QRgD&)^#d+jU@h)(-U*L<)$ct^VS->dLrEB}Uz7Z~MIgw5pT>4}Ug}=|};$ zbhvog?&sU(M=Gn>J{IGnMqx0tI5e0np!`tG@>!u=^`K!4+U-?5AU``I3zIo#gJt`X zbb@S>bL$(SAJ!oiQ@2BofI=#WCo&@L3?u50YH*a;`gGc84^^fu_T9DF~owk-nYDg%0UH)R~x9amCl?kI1l8GCtA&Z_u>{TYWdzPwB)`Y0TrA8Ew%#-EW^JO{VF4sjY>Tp(It>0<0}Um3I{2s02+ z3T`?VE8e_>-FOIy`S%Mxdv&lHav}QS@0K{mto@wgw-57S*YuqV z*cIvqpJTYxex!o|^9L|T21>u<@7{axRtsDbGkfr$G4JxGIQ*Z`FYY}jzsXOWev@OH z3^5blK;bBa)+G&Zs6#aeV5(O=X zeQlUMmTHMhh6^I+v_03wdO!PmI+w)}KMD@C9FzRULD%uX3ficf(8>EjM?!XYW=u2E zE!2Ka`Q?xVux-weX^x)P6^&8zf0o8n75Ln_B;mKyiKv-0=>#XsvAZZbOqdrP=(njQ z!N$hu4_=IgWOlNXrIZH!1yQN;n?pK9m3|}>-yHGl{LcG*yAB=KHd>F4Q1v#vn)!mkTV`fa?k1QDDSw zJuIq}#fI1%JTb$W0(KPX6K8!s1Mm#2th62OGO z^a}SW9Hud$zWcrg^&s`lvCiOPHCPHCjm70WTSMp(7dS&k0x~F~!Pn^NX_&Wghc(01 zuu5|~LLgIk;X&Za*6e|c0ui4z0s9f)N{1k)EQjU)gvK}w#*B$8tW~-9 z{AC*-DP14Qq2T65O>$x;jV3`%65cC6UnW|dI~5hv77o1-6KD`XdQJ`pa+k;xga1u#DT7Y%<>Yc^92VdnPtCLjx&cZ{kXH$PP^fBgh0 z9!fiDt1er+mBu62k@fq}L%#3z!O_0hl;e`jG#%LSmKro!>gQ0!Q06`Ry-% zonz3U=Kw_>;cBRx7|jU6MJju-^0i%l&01}1yd10TNzPZreWL=mQGrLKS}0GWYZV_9 zD~zMp(Tw0cgbqPNd@ypLYdPz0LYXy6y~sR1K}+?)@A-oED-UNX7r?oU_{lV6|03eUaiMRefy?XiwmFC9QU zhL!BkV9ReRecS>fZVRGM1T`$OFfDEMF9>=GFUVk@dGVxJ`ZW%FDXsxOre2_>1CM`3ZNQFDk|<}Awb9nPKS9>aj&VN(Rx_0G>OyV5mb82Jn<|6 zwCv2@ikQB$A$3|osDThHR`?K>icYYchl_$P%u9jmF5r6@ts4PXKgI_#tskV~hQ-Rd z<@nQs!@~`uVDGvg1c>&>osIhA8!!mu0nPRg(Wd|a3&w#o&1`pizd--%_go$HP^fXl z)$-6Ys&OFo)H-JIIwk}aC?6U+9)n|pQmp8y0cjKCBV70E`W)p7ze|CdH`b|Y3H9Th(%ybMUQyr z%+SzKVX5ub!KE5X^~=Wl^%HH^;?CE_g3j4#CH`qSWN}ok!g0E-8!QVDAybfEgKhbY z-Htq%_F&nw4_AWWzz1Ov7n*uP+x;6+fhF}=*}Fh;ChI|uQE?Z*Do6{Pp4SjsQ^{ro z@~cBb7U2~x0d?aAowTJ!(%wIN$pFhQZd3SCbDqI*)KeyZ^A-3OCk>J~R4R_q=-MmS zlw2V~n5z9Ns42ZEsCew3{`Kp-R7-u@{^@&LwlNBzT~QFb5>+8DDEkusM_lO2?^h#K*)?Tv0LxlLHvc;TD*-8XqxTIZuUNc!XZN zTGUd@{pLr^Y25s$Sxyuom{T1;qG?#UK*lI z&$a(xAQJ&q5?C4l%mLJ4xPpPQ>fynJ(E9Y*8Kk{tG@Q(*RH-SM1!u}YREHJ?V!cqq z`9kwwX-F{{MS)^Q)ja{rV=NUFHqHJtQZjL2eYvpTucEf7{Ea`N3l0Mg-~_z66An`s^+{wDzKN5ZA>_(Yt&OGgBJii7lO z@Ec(<_fwaBGJJL}>;?Z?nFSLYy~ZtJrsnAQ?{p5y1IIx;Q=hr&_1tr)&h#qhLVXDW zC7Wr>sdAHlQtc2mn_`cEboPq*C>MSKoTV;9f`Z8%3aTiIAvOz0f^E;vWOg!C9=4%6 zyyxfGm7w!I5$fQTlgcLYmde<2o|S48irm^Dr2;rxJ)Hr@?!O4#d%tH+-)=3gRMf_1q6 zfPj8$yAK45zIR?2+s>(~su}}F@S*dtWBY5htLs{d2Hz^I9k^}3IPJE5BpRH)`i`wX zLgYr5T7nK;+-be;xa~*#oc!6D6TxL2TVdMq4~&`(63rAl|1TPJjtM5K<2ohFN?&uz zIZQ69q^8=ny`6vGMG3``A2bsF49r)g#?dOwzSKe!QupJ`99KdQMty@t8hb8^J9Hm7 z(Jr36_AXqt*)m2|I#n{R6xssXRmv~EJfZ2$Qg}hmz9|sHb>*5`jFN&h?-fj#3F%r! zZi2e>EahUN7!m-*cH(W{E;jo9%x9$wFz+S!DdEFVkNr+OGiMFUpoBK!g(m-h&|5cj zsDg4*D_oQYJuv`6X6J3;7P)uB0TolFFPw{Ym>4<=#sZp~L(L{OZry(UJ1Nj6d6grBoppQEz zvtMBOahdz-kLlBj8M!ZCg)`y`MGy?j0Gb9pXfTVP=}bk;bdi!9=71ppK)!&ThF0p1 z6nWG-6BgT1aZR-{T&c#4P5*pqZyr z#{$H^j9>lr&oM9Lm!@WFWj-d{0`He52Gvmx zi(_ldJC$QG`mGI2l)=i#59U+#~&62m%X<8E`l1 z1|(oyq_^mi@@Zn>nfYzDTmyWcfhY+k9Q?fC>a2`E4Apes$Hyj?qGB?_f0z`28xG?& z5-QY3S6+#N5>*xIJMJs{kLmJ)JV~BhKnfv<99zSo(q{w#UC=FSmqun9*f5{u&@521 z8QWbeL#*1Rt=s|M07C&g$Qub5P_;8ENbLMXlXsmXugWWRl9M?K#N6k@xzK1XE;Lst z1s3gZgc7)-^zjHVpV13wh6C$BADxt6x~~Au?nj&nOuBI$Y$LKmqGqLcAwkm{$zG&F zDKXVWgFbv5k_(>HvhkqNF%)zTTuiRM#!|OAl2f7qbN3)Ek{-?k@mUdS#qz0% z2U@l(EEZz@sHCm-5vOlyejT?-G^|uX2?AVPQd|ru<9YQfrgwtJeJQT|RQ*p^(l59I zQ`IP|0~ewHxySR@U}(WTix|DDb&+6D?g)Ymmv@(=ORB4{b&tk2!qg&n?!8;Tb2#je z?S;dnuhf$Qhs-Yau5X{;>)Fft6KT_48kJuRV_E6H-@kvKG5c^*YIor7%0Pfhv~4;Y z{Yq}d?E70k?|84v!rIGbO_du!R^=c`12P?5L(sZZAx=QR0ayXM$h2$~6#!)m)S*xj z4oIN6=>6l!_V$tMU}AQyAd!&VOE3^54DHW4Iu5N|-CquZ791$9#rhxJ`tRSjt#bU^ zK@xz!dZE>3;^fJbLan^g+;$!BL+$C3?ZaYv>7;d3f1-rGQPFZt@CDH_z(5eiyx>P| zGJX_#bk6#8`wtF`X%vjL$XBX$hC*%Y!`6{HbX*4|&PZfYA>GCmyuZ0LuOlAh%@V{( z8kg1hPhqWitj+{gIP!sjbC@C^K#`SxlnX~WI%-8aBFt&{MKtJK^4=>NibR-*b|8q= z3lM*k%`vgEe3p>F7B8V@ncFLFVu{z}A+mJ{YGC3hZZO8cp*+Shkn zi{f9rxLjr8h>;hS_nlaH5Uy}jcBu5D(82c7>4*0QN8Ek+4i`ab$W+G(HboA}q)%Vk zjD^7{pq`p8CdO4oUR)lfDGG~Ws{A4+qY<%~Pw`5NptgtD9?pV@#Rs??am>uH$q6Px zupei!-kG}F(s1f3hEK7tr7HZ#12&kh2V4MN{)I93_|z#8_$@f$Sr;S_p69^j(aUeq z#t9Lz)oOk)D3PA!YdeL7zN37lV(oCPaKM#`*E$&USf?im!?j!i%R;2}n^L->=A!?X zRNPdq)Y2Hb(QQ)~ncwkm^x$`jPAFH*$Dvhib8ePVT){f1_$&D*sOn!@{PQ*(ramWH?zQg-d|6JGe<1}I)e+N zemLGoKU0J6^!Yp0NZl+-`d9(WuOQ|L5^wG?36OJ#9)^)(FqvHMKb(PAXz< z7YG71nK%5Z`fw>s1OeVvf6_4vww1~Cu>A4vR-d@42V=(ONA+!9vK}GyePchMg568h%|DGA&~BZ*dpb9nri>C zqYM+}H(>}ui7E#a%P6}Pm6(Om;)i*I@{>5V7_K~=j?T4K)mE{vgd@_23nJ{i-bS3x z?2;~w7h9BQBPC!$bnqNwLs;Ghz+%B#VF`cU0DIWP?5*ETKxfkk0xYWR(3m!u+;=+p z^;*Nk=Qw_YIEhFa&Fp54K5&TJ2SpbE51BDo^jrE6zwMXu)S9{r-DYP=|K)9-fg z%k`>CS|~O!)RrfHl*)0qL#+fW2Zx!jvS=I@GZUvjAl^48L8A6rGtX2)C0@1EVHrH# zuKz|~s8xerSWGnj!%2?_M8@meWK*~exzDwtk1abOfZBVf-zOc>_W(^h0JjiEaxFxR zGIX3h9T8m-+F#^}uuvv`4Q&T(UZ-;D7iv zT($|6s%C_<^BdHb!vGX(9oq|$>q%;#_VjkCmXVTbu^ai@{C9lGJ~XtXHYCkus#0wB zk^`62Od5Jtvrd(RW&NmD+TBXE(1EGP)OGAQr&eZxpw!_m7!mYMU^hmQiL~; z#@DtPe0C!ZK~3urbXWoK*}~J$lB_bP&4A4FH9Pua7^a=)pw@xu#-J4tD2dPf_A{jL zGXa(g|D-s-1{snsFRjRgVnP8B(xapv@nHS4vyGpdxb02`*5|S%g^^t54}KRUs9n9$Wg&I6#f*S zv*<$Qsh>$i(Xmq%I1s2&-*vVBdpoBEJbctkHtbcR0uc_Qwl3U_K(G|K@$%8MshVJt zIUk@}*$_7f2jC6(Fn9pbWHWtilN0F<5R*Tk_A5iH?_za1^9TrBlfp<(UOu=T@>1u^ z-2$mTsZRrUeTA*XzrLhY$mT|4sjz6Smk{4sFVX(bvtEXt!9sxeq6;2P|8~}6rYF%W ziuBgTP|w|pmxjjyypu|tRaHU+@kgq zm5St+QA$Zni-*Uv2B5K@E4En{EWjw{_siFn4EuRw3P6{X)Tg+G`wpHqxU84^qPKIV9 z&6-QQ63XFAd>#Zn_UrTN*@VJUTvb>X5@X;f9^26soPK<46 z_`a`iy}5B_Qc7l0pwY_`Ya@(j zqJOyVnXJrzj+>t_h}eFME0vv^I_vduo~$EB824)zM;@Qc&o*3d!>r>J;BYmmua-I{ z&97ov{+wq=t6u9tM=M*q&ZywGHd2?ImX;)HYkci3?p;uI9;2x(X|yGQ++qrPhBw*i z^viP9A_0DEOo%vQm%g3LtftpS%zYP=r6+&>O`l%9-r27kbLw$|_+{8W?AYFF+_!;; zOHfpB;ooc_E>SKr`szdW!hQFvj?GM`7g(pS4%?Yob#^y~zb3OISbXJM?eyDanE`a0 zMd#c1&tlbB6-g_v5a!UH&`@iZpthPJlflo&Dk(L6G33znHNpE) z+TmH5rLxJgDj9T!==v^>&C~k&zp=^gSbeYlGv_ns`Y=!7;J3F6?8oIXREh-KD$Pk> zLthfw`z197BSAz@z#CS>_3|9`g>^ZS`Q z2w>kL6fKHKGqBEv`~tc# z$Bo4a_bz;L;K8Pppd%uJoYp6vm!8GxwMowG^=o^gW>*q0bEXJwNc;+d%bQ1rS{2o{h*W?m5bA zjV;raSes$l@vZmjm&!d}gP-1vjMn+o4^EBf57*{9NGw~XoY-*6T>(^-oO<;f_m4rY zHN|wBC^X1L@54lR@8Hm6Z+jPMFrL5j(+!TzqeFS-pcxIv^R4pDPn|xkg>02XrJ!JD zT%7t~Z8vso19tr$n%d5M^ZK0ulRM1#pPQ$-8HS8T)%#6sbLK@x(vFnqzn1FVa-r!S zSR;6REBm2wR{p;mvvy`0tqpk9#Ma5&XLGaB&MLaxfNk-T%@xMxIx3R-!k13wTivu} z+A>#;T|2+%#cuN}Yn4}|Tu^dxJ+*;Lc!RL}{#dn>!ui45+Vy)b4(iX)3@Nsn&FZLs zh7907S_&XcKhBV8$V{gX$yGB%ncDbTPjvqdrg+9Hj?Av&p87{_cFJZBZ!%cHY+d5N zf)i*A#tbeS?q<`WiCg)0sCtoldY)fPO=hdZxad2%O$9p~!o1s}b|~|4XDwFeV~DbJ zn31fXFB|QRn4#>D(!OA{J0iqDS)3)FLXOHMPK_k?jE@YK#5OHHaPi_rDr~}8l29q^ zIMhFD18G@Fy0Mj5KvU#<->-PHnHi9t3_q2%o*m+Ea%jcH3!H?5vR~271!qR=a}MSn zoGrKwMy|~@53Q3qp*1#saC?r(-~`^VjGCLCyDX&bi?~}uLM6xGJB@8Bz`fRlZ&O&j zO8v*syyA0q2feuLY|wFjW@>&FjzBVPWXcr{m=X?QoR6z7UvD_vh4Lc# zLuJ)AJ$!SrOP->8r223ZR|MDGxmq$>iXpr^v9-5KigW-)qk!d79vcXFtd#PQ6?}Q& zaNP6X8&x@aT$?rG8kYY(^!P$YR@_k4g(Dy1c=x$hKmVpwP7|;GdjqZNnfixb@Icua zJ=gu`@N_t;In8@q)UZ^_!=L*J><5EaO2V$hZO9XtvGm8{7u?)0d}qsSopV*(=9Rdq z%$8=w}dHly4$>Q+~ zUt^X%{CQkB*F1WWfBFKERZo|jYe#Lj^}V~(!rp&(v?97UUfFm2AM-UA*pbJ>w(SY2 zXDcz@n6H05QOzINl(x2bVW!o}S*tx6s z-Cw>0jSx_P-+8jruJe>eQ{L^j=rEPcReXzMQBwe`9uroh*AR@Ppo6 z*IioonT<_1@^|iXcxT>dNvr2$MHz=}+q!U}f#E&}eyzV7AMO)0gm%+ryX;n;1NBd* zkD@4z1AjlZJnxwvnAY-Zdk{ZSfC{Ep(-p2+zi#5yI@0`hpF>vvNwRm+f6-_7B0Lu? ztI1KdoCBgg)7_pF{lG^Y#yMafT2qaIcBWPOEw-tBf45la(>;YJxvJj351rpQA6uxx z91#OfoQ26&c(xBu1H_rZ?Cqkn7^|4tb^^25N9M6ZW@ayImCCDTD6RiTL-U-3_1d_|C)IIWA7ACT&b8;;*sG%W{wYnI z=HzmbmlH}bth7&EL&F9etJn=d70!Ct&!wPyOy+eK(&v-6g2ppBlJ@#VH8q1q$qO}biv1zaMEr$G<%R23O1{C=WY>$PUz-1yq>ZXi2NDwXbk>>6PhKxE$Q~@cQ0=WM%Axn}6xV zzU5Jg8u6M`>fr9ZTN1{F8kL;xM!DY0GtFkfC9PzpvJGr^u=L`9-#3WDNBa^S&r+6k z@8kGrG50Vx@i4zIv>zVqxzFY^*eW8jaA%0`JZ1=eB^ivcla71^ErTw#{f%7K+${e4 zO-Hi~1n+!ZEx2=5y5CI7+1a2~wXk4?iw;~iT4x6rTib({dMdnq0ricM(z|P8}%7)j~Qs}B7waK(!j%#C{ zgvAzm^uJp#q>5G;+%)3l*H%jVeLUN^hZ*2~S&`}hSdQSMHg?It74*u;o7DfJZA zbia_W3OBA4!YiIX+@uE0KY zd!t33xobA$FiIGLF(GP*Mp0yCn&o1jJNSLq(9qP+U3jst1$n~EtfMCuW(Zjxy}#{m zp@_)HZCdPnRX+c#>3nRXB|9>%FT2j8;Xy&T*41VRH-7Acw_>^}qxiwobgo8dR zR~T=Y`TT#gn{?+Kd-vst9Zs7b6@Be%bYD)yfRWFU51IW}zaxiETx>_d!`4u6t0UOu&W|Gt$-8Se7e>KbK}zG4YWO@*haJ1wH2Y( zvsN2OWYTl{4%ilOyeb&{uhZaYR@2$Ffj2{PlR{j!Gn3w~zRbKpuMP>bVw6O)mDODX z_if^_l%C06nzf#Rn+V#v>iXi;0M<6q;O(N#L)gV0#j3!isDE!p(gxFGZj&KUnfX=6 zvep+UN0H%IUj3I2OH0wTc~-htw;}Zo!aQ&iOR11_Q)LH3e6H+`MJYqyEjmB!i81}P z*}YHU`g4VwTXPI6m1b!-3*F>;8oblcQKUM=&nhL|FZpJOAybJ>f3MR%gjN|*VkcYi z+RuAjOMfO#3Lg-BGE~m-$cBfKmGW3ea!GAke^*$om^0VTS?%yv9v&yrl04t#9-G{I z<|xH(w3{6*v1a$iw)^lN&G>`ASn-?r_8q8m^u77;6!4>g6{2IDfdUXmXcCf-X`;RHGYQAQ!TDgA~ z^Qv%L{Q~`|__Ff$iHyW#71)K#BfEHqrimM+eP+KlQxIQC_e3;Cu zd85~Kz?6>PWUmsL_tO4S=%NVzJ4LY%iuK$rIC>ntwBq82U+-=?rKHkdqa`bBt$b=C ze5zq;OuVC~HGZv%bsdMs5{DYOFJ-P8OSt1lH%NW^?BjJhBj=#PikbUKFv1X3;bv}Q zipV&wKEhr-k1erZyV;=ox?i$m2K_emf3ogvUHd8<`#FCL^P4-oSj^UCDhb_W`qP!8 z?%Yxai$tFWAm?bp7K|iD^Rk`DHz>9E^Rm|BZNYM4TnZ$8p?*H z5dIuN1GBT}#qTlt<+dlbmfotXSmys*qsI`XblhI19t~1Pe%{s$V@D!Q^Z4u|I|SB; zvLQlm5?H^?HkQzKM&0tmzVKxaPk(7h@cOqp!RK?*9{0sr<_eiKU$a#J=YT%z%2b?H-@`S7RaD+b*~%dgG4W4?ACbeUPpB&#rGC|;!f6~p~9@Wb9IC+ zEk|+-&7tD=!AC*2QR-<|6)<1y0V*xXq#2Zf(Iv+_=BQ`o2L?I6Q{)TcMN2VBp@^w1 zDpNwk*512ANzd7Hzh8#?=$5&4ecg~SRr{-sb?UureOJto=jlG7o;0xs&R@=#{2mp= z0JZjy;R!woWPpgXvv0BMuYZ{d6T{VnhcGMuUHuHnKFGD8{=pz=t|d9uFS(0}E|!+N zv1N9?J&JY5c#UzU=rmi`y5}`<=Caf%Y7)@sHq&Hk_*Hq#$wFwiInzNWhr5KK(TFM7ps}3ftjC`oDq4nJknCGqdP#hRAqUVq@d~X#8_U=0E>O;|V8b8fdF0u|u_Zm{jXX zi~!*9;oWKZO*gj*-^_V=6k8XD%#Vuqdd~sr2q%^z>OPk6?c69Y6%p6yaroS2F%#}K z8T6s8%0mwwe7-81SjcqgxfENw0?p(T@&Pp&0losNGYk^ z?|{O$uXDGrBWX5QcTo0Yz4b$MsbOzCFXq0pzF2Xe(oQ=^T2a1S-eDiM_$hEZO#Ao!v>#rs|f9O#&beuLDC$H2o`JsXNx(!<`Q++s+ z^-taS!s#sPx86s^*{u&IJLl&d9Dfr~v!V2i@P6n0^STcdiXANW_9qGswj$++O(&8g zC8gi*SBB$I)t9kl#AR>wZ2ZU(PdL}Bp5Q%t?BQLo73HXd*~saTh_w{kdxC;@MiYWo zUcR=gwb(uT`?qi7-Ql=|n4#al51q*3=he10cy|0X`?Z0rIX~0vHG8&8{0-I}*Q~GJ zZGnsFuJ<_W&zBuPUv~0*`yQ?PK`*@XUzuh-Oi4*ywlwR0-?=X<{~A3sy5)Y~{;@66 zWB-))<St^-lIF zcC}ynqQ%8xruOg6=i3|YBS}YUYIYCx0N+p2W=KYl3|9&UIs(&CL`1g#0JISV*<0fcZCu=QK1S%yzIs_WOgJmVx-w^` z$m;h7ttPXyx8)d$EWva$W^3tYgiBIqlX1K^NE)Gz)FOF5Yuv++!y(0kC*d&=uqUzM z+d!!zSQM&Xe15xrE)5dF~ z41xc{*Uit=!+?h(w0#Tr{+WX9C4TOrLwWVqc?Qe^M)X>y?H;C}Ac(XvN$?|{tJTA1&1!IMwHHKBGs}_|tXcGf|3QjbI;I+=9T*yK4glGN>7(Ek)ML#T}j-*~)#>6uiU& zYTZttS7f1xh`H!_nUrAh^D@yoi*RW=c7{wFQTM+0wnco&0tjUzgO)4T@Gas3{t7-3 zs737!a59*8XK(%Kjg^E#!#APQqyRBq&tEFmS6L;(SdHOWvc<0Q{%L4L|FY}-ZcyEl z`CVJeX{(NGT})dtyJoez-I1C=l9NXqx$x3sdqfn<{>`VWRz0M*rn5{ZQbkQ|MQ6gBzDN3PwZTxDhJrL z#n^3)He}{7nb1;%?oKAaY+Ts%z&tzjv;6NHuoWPXzD7Tf=2)I)-Prq~EjuapK+q(= ziu7+DoDLJwz6Z4Wm|Ybpv*43W1ZxY6ADLnq&C<7vlFZ^@e2eR7X|=4d)HQjrZHYQI zkr7(6Sx<17<8279Hx02}Rh#%;ikwngpXb}ra30oyVjdeiY**c8iGvaRm_Q;f9{D)* zsfeG!;V*ye%L8Tg?O%rIFMwrP!kk%EjO{!p<>k+p3oCLStWyy$+BBtk?#EEd7tc4I zlM^HF+k5V2vNMYl-QKxXs=3L&<=MUh9+K{v=oE z=q*v9bI)p)eS7KbOLHqf{=#OKxf|~#*<-dac<iwEms$9g{mCEk@ zS8{=6+Tm;r6FfJUf#Y)5BF@PUeO5;MxsPK)>07s_MoLHDeHCql$y*kUe<{W5T`k`c zIfahZo@CS_p4lsW>DZ0W7#d zsW2mxUh1uv=$wxHZ0@kT6L6Bfd(>$|?DJwWRj&+L`24krS3aV3s;9iH>`>mqCY@vQqz z>G?u}r{OiPu$dLJxO=#*_53ipO?~eyHeX?gPA@O5=*^l{TtVN~3Zm&fC zf@4{h=WJu=pr};J4+ElplndmyoosegZ7uUjjEWL29gOe$A?q_BJ3f*)rEVb?V38{F z5CtRsq`-gt)w&6xf-jDZ(Az-7Lj0sx@F&l-{3?wG&L9U@I?~}UfV01CC%+)at-8eA zLH>A_7LSEP7+B_8Q4}~_3OZt)pW1n7QMNLh`UwcnC{?)HTPjyb9H07Yp z4Vik>`I7fSH6G3u5$#!>dWChGMG_@soyqJ;`)H%j=wK{tIIqo8hBa5ZraH1L#!G&r zfqvzRw8gAQ$=%E7o*FgP%5vlOEHnByN@6LK6b+tSBqnRXCHm~{5go6g`xf(J|9hPy$8>c@+QDM zfG&u4S%CCLjMQ6!At@MAWct=F!Cu+bNut`v&|z~%e(9WXO|R|b(DC3i@1DY`U1%gU zDS6lvWgt^A@^cQ44;}Vd**;vg!J{YM{WAhdm<{LF)u$gmtiU$9jZD^hblRs56W?5u zikUE1w*&7;ud5CeCHEA!PbfrsOpf%R!yjm;gP!5R(!n6r9Vo(XtpT1=k2Y6NQ{zk~GXRfa0w6F6dV{0)7%Yf7n6g@X%`r2_#{4GqH>KXhtr+c( zNheY=UoE`}Y*4H<$+{ryVS)zzo5=1>;W1OQKJ%LS0ZvCv^@De6k{cM_v${Oc5; zp{8ao{y-jzB84WrT5#q3U|nO>*C{C_|4=ssfv4r+on}t zF;!&xZ*j1B?%Y+F?o+z%!aP>Zj#ZL-BA3o(ERW*bvPkPk&T_wSYy$IWNejlUq z`bu_e07e_lYs17j>~(%D>PhH|HaYZx{L6oH2y?g&_h6S6Bqj=RO60I5W{g9Ij!MkHr&y;2w{PFpe)XX3M3DVBkPc|xbfyHk@@aY__(bjUx0vnC?XoInS;mCNyuk7N zzV@C+L+`uLT?doy>i0WzlN#M1UTg*Bn@3=&S?ImXSMDplu|Z#_xQ0+e04eaMq*_HN?lV$Z#1czg~Nv-}Z8; z=y-wd!npfHJHjxgUk&ewS!fPnq=M_?2A{zVWcCtft1M>K#BXwkSB1ZV*@OFb4-a35 z-vk&q&gehxg>CI)q$7;_v~_?=@EEW-?Al* z0=sFE7het!XQc&S0>o0l_t0owv926;@p2Aj*XX%Oh*wKX1?^V$=R|}8HTVc;l&ayC ziV9t>{8>5j5Bo*AvH7q)ob(SXaIf;n$#-fp>(_C*DQdYgK%NZDD&nJZLA#(OmN zu4tjX*X~Y@f-ll9n?IGQMyl-*3g0divm#hO@@%iSwG7hhV()~m*! z*a(FDLJlW#2KDSQOQs6LQjceO^_^?)B6WzQ2+%b9jbq_~-uAMFhH*}V;^mcD>m3_) zlB51Uv8ccdz(|qQC1w3fka8jW8Xzy^#NrJZxZU*p0gO~t*((g-B1{2Cg8?y+=Rsq+ zfRi_5wj4_pQAi>BVgzi#vgRpAX;VrPn-+|p!Pu*s6FZA8Gb=(&M*TC?!6~X(2;-w+Kw`qBM zA@_rUClVI5?Cz8z?h+*T?W*8Gd>83)F!J7W+vc4@#g2{5b}=U$^XgQ^a-MG&DrmL^ z(c0Jo?V)}GW`WbJ&~Y3++ASsZSvuz)0EGBSih9)NK3AZAH}Bqaz$}{Qlu?}u`=fBq zp%tArkKG`Le?TNa$>EUTx9j(`pTIArtf>8cbc}v)_Q@F=e0xrm@oLQRn)g8N%WUV@zt?xn zc$@cVrt#JRrIyoK>)l|w1mtlQXkr*?3i^!e3XmpJh|sc+t;>Dx2~&kZ$Dc^JO1TK) z7mB6(mq&Um#3d717pRAWx`3ku!2a(+!UEQ@mk2o-By1cn3tgG}N0B>^+}N0I|Z-BUtiPr>66Ke;odwPp2e^*n^IM59Ku*IM`C`ix@pp zr6jU=YggQ66-%U~9CZp*0(MHB0D>WtCxOne?|bH12sf-Hc9Hi8=AC{Le+&bi4-X4e zG?(sAmp&y(f~jEr8z&YKRUrnuEat?rXgFJdokl2Db9eF*geq|QKCWlaj%qp;woDCz zzK#XSN@H_Wc;Lw#NxrQ7%`IgudUvu=PVPju@8*uh zFwmKrv%TCBdK%tNDzzlezRNx5@s=wEv!1!VU9<^kP_G&Z*7hGb0w(Lg%@7)>ho4X= zSJi*28Ury?68HjHUD|CG1H+}XZf?uBNRpHS`o^CCza-&ma}|;HPM9MlSR_G+YuCXq zQepG7jH4>TrJxBp&+ihDLfE;iic$pFyd!{*I$;QoueNbxGCVE3GV){j#$cO8&;Hu0 z5dt}bj}tNeuQMg-53*N8!gNqF4u)J!8z+QCaUx~-FZ6Psa5&!AO7*B`pdCrb5U)}mP1?@3$nT1Ajikwp;B;o_Mw@FW^dv5s{OtAfz_C=B!Vu& ziwN{hThNMNkZ>wX*rE}1Vp+fmpaFT?BKelLpDdw0pX7-!Jb@jMJ+KrcnvxKKQo^O) z+Qm@U3{HkITdv}Q+ra2g6Tj((`yseO3m}d3puW1H`E+%$SfmAM-F8poW3>fwB%Aj<}tlbcLh+6pr#H}@1f=4rCkf^X` z5tV}$a7BF)*c)+1B$1!Xa&QBl z5(VmeKm?E&%X~dgXWW`_Gx!s@0och!N$=sZgoK1icQT89egE}H`^ijKFE9D6zK&u# z4jrpH23x*^f8-S$o^{zb|GU+>hTrpeTRN=^%t$DuMfK9Phsr-kZWoXxU=@q*clKC) zs5`mR`B+MfQ%CTcOEjBY3+0afmQI+B>;H`94U>k^SWQ#LX-*0s3!^6F16-PTM3Uni zSKMud+#d)Ryd)xG;3-sW+H!!Wm}`e1*gn=G@-5*s(15ykj;daRvF#tx*vV_8?y#N2 zE-KlL1|5hyCB1%dHU5=@sktP5>=sRk39=dYC|ml7G!z2g1p)%3`zL}Cd_!hHVN?zP ztOJKnUmvghpBRzw338!-vCI~AB=H`<3M@r(}a|EEsnNO>x2GJja5=7hxjtRMk z^8{I>Ul9HL1O;f=5}Nb*XB|JyWV$w{!c3l=9$-ouj^`ZnpD zqgnk==db%g>XK2+A86U(nm8tb%^%2ym=U}T2tjwPC^o4PX~ES{u^S1aPmxcP%Zkp_nkZn3)_a8GXXhwR52n->??oJT;RRrgxNU8t{ou~>a zDPYGbxX*YPAIUL;*F&pLCfF0sL0*m^P!DL+qJm&v2#t#Ou?!IvDa3Pl+#uiz1jP#@ zKiF(bWR4&J%LL}}Ns2&IE<#8oLCDGalF@%N|4X+4C+=N=W7#Q0@EerOOz;9?4D$UK zywTwQXHfHr!^MEHAVS~(3O&T?NJ4-zrc2<92)2n}M=n~y|MuTU5m&eq&JUIhy+3Co z1J;N)hVP36@gPz9qQ0`JE!engb!|8WrP-)+Fi?8S+{g#=B;bLYC>ZH=FRSaD%Jiv> z-|&9sf{9fe!Id?8v zKCl~GrIWp@!Xfey7ucYvU;~%`cgL(*3$yY&=8$*-j_${Df?F~Ffh4gUM9;vv6R;5E zP>@*DK=z;bIO5;-AnYKKIB{-}C*__XL?}2681_S?=tk15(h5>g1DF@Y;#2|Y} z%!owTR9sh0L`?++u;-mZnvS5lNUggS6P5mH@WofsL&<8i#fJ|QaiK>^+;4SbChI~9q; zp<&}ka?v#TAUx$D0yip87EYXY7?=pM!bsRidz%vP62z-32YVsp?K4Tl)LTP${x~Fx zD&a?zyAPADM?e9HqtY>yO)2Qa?GlQa&e4J#{4pq`Zo8n4#0%h_{mCCAP4E`?EDsdj zwJO*GByR;a&;$v!M`on3aF<;!;_2yxCKZ9f!WUvA4+BfXK=sK^6?p!9lJ9_S@52*- z-S<9I=PzG)TCHnI57_kBVvF0Dn?&2AG2u65!<9b$8UFr0^S#1%UDX)eF@$}&f3454 zJkQVqV*)KbR!JQS3u@4==#lDde0oXD!>pcc6pIUS9?SXvUXBW1H4_)NS7o*}~8L!ve~sxMN00Vfcc5*d&<94I)= zz`}*_yPntK+K4+R@TajB?@khD5E=zkiGMdOE?2xa4Xz$EhzRL261U(Lh^67Z@RkJK zNVWkAu!q=^2Ek_%Pf+O;l71o48?o}PE4Vp=-HlSgb&(a?#LSVZzl%&XDn=-_j|YlM zVvmsuI-v>ms+SO=T_Bo4aFLO4uR~IP-S{$m zJ@=67=|P#snz^1{oK2}e>ihdAZR6 z1vc1z5>p-=j65V6_#xktc4)=|-}6UiVK<0uQKXDbHJ2CK?QJYqY$)#QnP__Bkao`P z`jewGgrv6_i6jTfXs;2GJE>=8Bz~=XH#@yk&DgQ7XJpovJ?!z7ULD-Q(JsNvyKkBA zgRU)&Il5E+Qm9nmij|JDt-tYdao?Qtnf2w+qtYQ&$pcQWW0;x6CVh50Iv-Zi!G zYI8y|lCAciy0hJu{XjB0WN)Q^`awoch5?JADq2lfF!GGV7eo7%a5j=2t2BE_bXe^p z#=%=grwl7?88Xpz3`aUnPG?xLuUFCZtoiWNIm3)$#q{*IjC6_p%?v$Jd{ony?F^`8 zW@yZ%?+>VrN;XPzSx2&AqrbkUOJsVO(ql3VGOjZiO=r5V*Y4ErzOEZk%Q&UiEfsA| z7t4^?Z>Vmp#JuyhmO(BSN}Q!i15@z9~r@Ctm30)gb(9q`}7<6%@VyEU^xaVM~fzkAKXS zpYj`(t)^+JRe78YJ!O=Wk#71XdT-@lRY%I&UGF60-5C;@*BNwGQ89AW%d~7E1xG9} zF6@C=bakpxk}~sPTBkSGKSalNvGqMe^)wev(IH%}#QqEed~*k4fq3Rc&x_X4*4FM; z#zJ3bY*|f7EJZm(kB-GWW5L4rvO3+z3?K!AqEHBmk>wdq6qa||=^ zo;tQ{1959-afk)FfMsT;AH3yaSb17aA{}p}fJ@ep6QxJh;sitC$u=_5v=eQ8Ka$Vi zzJ;E#Fl_F_DMIgfvgu2zZpcIiL>i2=5Nl{gNbN;t<*fn5(t+sg9|y{Q9q<}$OtdR( z$!YJyWy=szTYr7%;_Y3J*C%`D7ADthKbTg2aK33}wumeg)I$t6-%PYhh>?aup23*O ze-C$uHJrcPvcYLUbJOPTrc3JvK7ElpTy{RT{bHm39a`w^nMs7a_vEMVOE8lqeI@b0&v za}aesv6gDh$x~DpPAs~baZbkdrZcIXmgySDa1{A&^7d46A27r0l7V@eKX+(WEO9D% z0(MW$QKKx)z%b|T8iJ!umb7xoj8%xv8PhQ)}fNitV@#L&sa2B1n9tGetnp%T(wo( zc7IL=U`kIkCgVY3NEjeTJUXNE5aV*j!DtAKO;@FdC&LiafdhaxRl12`|8h}!H6>MT z(f!&m4SYn^`lovtWPPp8nFn-C!yjCSm<70_Ww&+v?@sMxGkUdCJEfxQaKo1~9O=L< zVsRl9YSQoOcC#5UQEO59{>n$`WMhEr+))R4hl<;6Ele0`0JzwYkT2LO_<(jMWb2pI z1&~4J&~UP9q5_$CgG>WY5$GOXJHv`X?ERHUYEBKKkpn5MAOo>Tc$kM7tTg}f509}D zJv}`oW4*<3adFOS5)V+ZLWxRUQ?m!Bzp10HkQOB<28{F)&I0(Ls;6uiu)Qd0%1ipJSm;tDHbG0aqhKzCkY5jnP}FERYyXfvaCJDWvJKUz1QGg?4X%l3pfY#45;mbXCqW7E#zDVVMwq6g|nE>JA&r6 z+LK>2CvFolZBI?Mde=Af&21Aoz)2>pl#aFakvg9Jr%(@ZvakzP&L2j45_@oLAFkLo zMY-yTQBfisbGKnSIEjK$ktE!7SS@MPJ{dmMR93kOHr_rdJ2iLHq*(ER!Uwr?SKq3mJK8=sZwVm7rU4zugjd&9+GwNRsluB|D6{W8$9hOQQ{pa>DD!v*}7nxyt zEdb0#RJ1Pl$E!W-NE%Hs1I%);L9?NH%WoyTIPSChnr%`ioh}nigl$wbk6@j%UTWtY zpU)SE+b4gO)jLi7c5?k%cleXYTP|Rz*F?05$Ka>EhuXjYZg_HA|3e?QPt2SveIrwY zmFv7uxx2Uw_*;kim`!M|tsAJm5K~2aGu*>q&=*|frclG68S0#gbM<&TqGcJK*9me# zyK_Q->aTDqbEX$E4A_=y=t%?lV&oV$Gg|j(Yf}yx4hkP&8e_*SOjJebusq6ldAuXu zMPPCC&<)+no!ypDHeiR@BDgcc6yc6S_TnWF2Ur;u(W5WX71$m(7ztP5A2dgIO$9l5 z55|(>U(hsoWfbjM19$KZY=k85$0icmPF7Qb0k77h(?uIW&Klu&L}S!9ZR+~?ktZ64 zlNfRvEP|o28*fF%!;*r9($Ux1ge4Hf+=hF|(5uTOY>;&4A-)O>frKc!m3To?ENDv5 zAAAYK=Mo&L24A$bH6LYW=Titprm8}P-J?;gJ~jpDyH(;0#i?ccbv0ylgoeVb=ai ze})Evg;W^4Xye1Xaq;o}Sx)E4s3^mvTyX-6g)IcHKT%fOmeK=efU&|e&0}Gf@IfJ z$vz#tCKZ@Z1z;g3~IbK�X3mg(EL}t58447; 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 z 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%0ARvA8@iZ1yz?(*sqO77$dDDu%(}n%p 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 Date: Tue, 31 Oct 2023 22:28:48 +0100 Subject: [PATCH 29/64] [mybmw] change handling of TimeZoneProvider Signed-off-by: Martin Grassl --- .../mybmw/internal/handler/VehicleHandler.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java index b28406853d1fb..ccb8dfeadfa83 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java @@ -193,7 +193,7 @@ public class VehicleHandler extends BaseThingHandler { private MyBMWCommandOptionProvider commandOptionProvider; private LocationProvider locationProvider; - private ZoneId timeZone; + private TimeZoneProvider timeZoneProvider; // Data Caches private Optional vehicleStatusCache = Optional.empty(); @@ -212,7 +212,7 @@ public VehicleHandler(Thing thing, MyBMWCommandOptionProvider commandOptionProvi super(thing); logger.trace("xxxVehicleHandler.constructor {}, {}", thing.getUID(), driveTrain); this.commandOptionProvider = commandOptionProvider; - this.timeZone = timeZoneProvider.getTimeZone(); + this.timeZoneProvider = timeZoneProvider; this.locationProvider = locationProvider; if (locationProvider.getLocation() == null) { logger.debug("Home location not available"); @@ -475,9 +475,11 @@ private void updateVehicleOverallStatus(VehicleState vehicleState, @Nullable Str updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL, Converter.toTitleCase(vehicleState.getOverallCheckControlStatus()), channelToBeUpdated); updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE, - Converter.zonedToLocalDateTime(vehicleState.getLastUpdatedAt(), timeZone), channelToBeUpdated); + Converter.zonedToLocalDateTime(vehicleState.getLastUpdatedAt(), timeZoneProvider.getTimeZone()), + channelToBeUpdated); updateChannel(CHANNEL_GROUP_STATUS, LAST_FETCHED, - Converter.zonedToLocalDateTime(vehicleState.getLastFetched(), timeZone), channelToBeUpdated); + Converter.zonedToLocalDateTime(vehicleState.getLastFetched(), timeZoneProvider.getTimeZone()), + channelToBeUpdated); updateChannel(CHANNEL_GROUP_STATUS, DOORS, Converter.toTitleCase(vehicleState.getDoorsState().getCombinedState()), channelToBeUpdated); updateChannel(CHANNEL_GROUP_STATUS, WINDOWS, @@ -661,7 +663,8 @@ private void selectService(int index, @Nullable String channelToBeUpdated) { updateChannel(CHANNEL_GROUP_SERVICE, DETAILS, StringType.valueOf(serviceEntry.getDescription()), channelToBeUpdated); updateChannel(CHANNEL_GROUP_SERVICE, DATE, - Converter.zonedToLocalDateTime(serviceEntry.getDateTime(), timeZone), channelToBeUpdated); + Converter.zonedToLocalDateTime(serviceEntry.getDateTime(), timeZoneProvider.getTimeZone()), + channelToBeUpdated); if (serviceEntry.getMileage() > 0) { updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE, From ca9c31e91a07d1b7b87f7bcbdb0a349f5cfd808a Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 1 Nov 2023 21:44:24 +0100 Subject: [PATCH 30/64] [mybmw] delete png file used in unit test Signed-off-by: Martin Grassl --- .../handler/backend/MyBMWHttpProxyTest.java | 3 +-- .../responses/MILD_HYBRID/340i_frontView.png | Bin 118802 -> 0 bytes 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_frontView.png diff --git a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java index b24fdbf6e033e..d49e33f37df90 100644 --- a/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java +++ b/bundles/org.openhab.binding.mybmw/src/test/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxyTest.java @@ -148,8 +148,7 @@ void testErrorPost() { @Test void testSuccessfulImage() { // test successful POST for remote service execution - byte[] responseContent = FileReader.fileToByteArray("responses/MILD_HYBRID/340i_frontView.png"); - MyBMWHttpProxy myBMWProxy = generateMyBmwProxy(200, new String(responseContent)); + MyBMWHttpProxy myBMWProxy = generateMyBmwProxy(200, "test"); try { byte[] image = myBMWProxy.requestImage("testVin", BimmerConstants.BRAND_BMW, new ImageProperties()); diff --git a/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_frontView.png b/bundles/org.openhab.binding.mybmw/src/test/resources/responses/MILD_HYBRID/340i_frontView.png deleted file mode 100644 index bb2f96f7f2261cf2a9f9830103628f643bd86877..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118802 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

      9+m9l7#C=0E!P!S_|C`Q3N!}jDx*J` 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

      Ak zh7ZC+sd2DejH7$dX<0B;AbhYULij9ih*tv0T4DMS{~P>?)ZG{&QcQDzFm=rHM{4+?b{Ma;$kluT-aBRtl$mE z^gyo1#8rp`y57+zBvI^wWH`6Xr0~|QO1unZ=9Eb@t>`rFv(+Q)Y1v&ky+Tr)HRb>r zzQI}86^mSrBb!`hV|@Ey8+p{qA9@7eg-+y(;2&Dzjum6kUGL#)mvnt?@7_3ECBL3# zlj(W0zq3+azN`7)Cx(=*MD|t6Grq$9`~C<5H|UodS)9oCMmos=L0Ug(o8zO?i3kG! zgHm@9BY4=)pA$e|6nKiTu4EK5W=J*?4;WYEA2~fAc?HqD)95WBKCv;RFKnmKM^QE-11nvrC{5bX1 zaj-U74|^db1-2?HbbRk$I?%4x7j(zxPgKm_Q>=#mFEsnI?o4uxT!{|2HWAtt6Povs z(ZiGoJDnt9HrlF|V)pW>S_-i}?OU+NTmP(T2c#aR)uWt)hUYc5wok951T1MZwy3Ry zQh&z>im>h%h1!y-c?7>9pCLryWw$vmbbPDUV#g7XiW1sS6dHuXi>Qtb;aQ=1z~kiC z%NiLOW#CbsT}1uB0k_)sP|1a051qy4gG$v<+Y>s42Zu~H48H#Wl9Qtqab3x~7scdM zvNy=e$|1?12w2(3f;b?|xyKL+oW;ZI*g656MQE{A>D5j#vNfg@%O!gpV*@V(UH*`2 zay8CelIu9epPh^?s>>lgE5^bPLC=ee-X(!pl1?5(A`Tqw5!f9dXMcg{g9SLTTlUO= zdj>sNeJ4LOvoqiZg`v=SF6bCFxG^xF@Tzk8^=N()fMbdT`~(ESt4i0iaX38=BujxO z=fIguC~~vhS+)$0z$3&{!eO^Rz(|PNhj75*0y;Z#9Xik`Y64m&8-@=?tuC)`Qb=)U zHfMfmq`f#WeV4ZSK5EJD3J1NWzq4v$?}HM6u5LtNN~XS;Q5E`>W*xJ>8jNQ(g3-MQ z6v!8_=1lYc!&hS(h1!3-K+WLC!A(2|0hmg8 z)Vf&*EwdBA7eO&hWa_^81wd7`QlDlE2$za5EEH@j9N2X=4x%x8ojy$#=8r=y|0rk; zxf7CFUf&znBELTv<%K5R)J`&17wy<1hB-h$jOPZ3<6%9HvEnUuyljH8^1sk2%OzVT zMgDs86oy=4X;+-14@O7A%HIZ)aunB$Bb~&M(-|9GY9PsVk#zEe-Daf3pG{l$J7nKbjP?=i6AVoZ{Sdz;1P%pm8mRJCKMh+7;s8bn%&)N?l?>W*dJ?m=k_VUx zhg^oB3<8WKQ;S;FeEC^f$ z@mP476EBp}VBc|)vG+i!ir@K@PUe^(jml1oa}eXo?F_^nlLWJVcJZgDJ)x+GrzQH* zC4}NHis2eKH5?|z^e=cbbW9SgxIIC>3%Cr}cmEoM6)>Uis;f{D?I#SfnZ2qF;|#Ut z2l^uCenB&_1`~_0%Qf`OeM;>hqr0C)?`2s#oj2jkq}8#3p!-P96m611>BBIt4}oh8dVOz3}9M2 zl{XPDy#Lht?%8FE&vmW+eY4uk4H4JxOy^B3<~Ff59dBYIM)M1)0b0BV<8v5z`>bZr zJI%g#325$r9n?6@LRbz!9gPbj0o8-sN0u4rAc9Y3rlrV$m;SUi-1du=`Uo_>W7!=>n0Beh;wyS zmJfcjfZ44`#?h*f5#b&4!hY~Ym;=bKX3PLPBzT}Re>LVa0cg`*U|D zNbJ3`B|4}{-G#}VwJ=5_x;8p2x{Ucda|6eN^>8FL#~x~YVN?TR27u2{U=Rn-D-C?E5^otwDRN9il&pUnm}yWNa|*bp zr)6s{b;{|&^OqWo1%uKs@cE#XzcLvsQg})dEOm-0$P6;c3{S%L)~R}=da#(Xpe&>~ z@+H80$XX!|02?5`9zYnpWen-$>C>%YL$f=;9YDijn?Ok#Si;#)G_hE@Y=rG+y&AJ6 zdb>q5KMcuc-^1z*57)=q$Gjzaw&`ljUe+x!WlVj{dzmyu-?{zl+{7o{#KZ&t=`hPG zFtMT*Qs%;(SO2eYYinCkNC-mO&Jdk8>K-Bu9Vj90Q60i+KSr&2PbroKCl=wvH@oY2o~zyx1~#|O0h7mom-gzP=U@KZL(a*3(_C6!1!BX z;7Lj)!A6#dan)S!l-tcEM<&37$*StX?-|Z4cr+-H0XhT`K(uEJ(+E0LL+bqf&u9M6 zF9xN8cp*{c9$Y^3d3^udHTGpZ;hXA?h+oey*Ku?ZUcpqXGX;^_Gx^2S9Okpu48Hsb z0yl&W31*9K<+D`7l_l?7i5EQB5SNz?P#n+&rdC@4tY!}Xr=P$&i^ zM|ME=au3KgK*#-l6wyGAvK7O31V;}#CK?i5?mnr_-HVaSY7Zbo7 z=@?P-hg0E)IQbAlOcY801A=w|5j(iYW>dEoM-_ivP)SU)L$>e!X9 zi65pm>|BUEgP*hlQh>L#gq;PS8Ct^ZHXlGMXFdig0<Yck-UA#+j!PEY4orzNx_Hn3sffdLp4!{Pv(<}!F$B}ncw@b2NB!9}tFOT#c$ z>KP2SgHRnzKPVMQJry1eSz=f6Cpp~e1KON$Q(197npn~7=l@9=hF;`{>#ywjz3x?% z^Fch2N)bE+`oOP3UK{KxfI7nT+)k4*d&HO+8jQV3pgxDUM3FwUkR>1oNQ9JSRZWnY zkm4!}8!H0;KD`L}cDQC5(yG_sT7w-mKsa##)HcM736wXQGdCn4QR)N| z&k*|mEBa&{%iyuMZh?(g10V+!F8LeBLd#+SE4BxI0NU~%k!rPqQ&mU+yKumZ3V+sE z0#xHdD2xyW=_I(Cju?pS;}C$LM>r(A3;yLTewT!WXJvR9SL3Yz#^*qq8KG?8V(^5` zvRuarRH@Ky3(l|BxE8t(Z9FL8fAo|voI?GMDO_SEpLN*TWYL~h1X@WsY6<5~zxRiv z0uVM7F9YHVeGnDH)4+D&#h`YhQ);x zBZ7cDJ^(~H8LqP0ec<>ZCW;GD|7$nQNo;62A;Ii)7aj^}JW@zD5ECF|$e)DR0f^+N z8mI%!2KYi(B}iT(?mK}Rv2*0I7TdC7jo8D(2YTGMWsQ&xnE7g+ompXlgH~^Ub zx}X0=CQswk62a|7H_1Q;pgs>VWH})INF2a%4HU729|)7GFt!Y z(nUxmSH1K4r1A@){oU0qEegx`&ux?5t7qT|na}JE5OI5rB3gelABT_)cmz5I_ytx4 zJ|GB;c2xr4Lg}xQgY2s+3hmm+0!=~p-M9!Mi=-Jy1>hb655#D}g#q|Z2e`Bc6oi{c z_=J2T3($L4A@Bo_ssrs~$ixd;|9`3ouL$LzdD8z@+lLyV)=>!U5@~IIZl6?U(V$7y;sxsjuLXd)2*Pv`qy&7{LPk^@B*mNpGIkS$ z=P%Ec1!e=I2SHK*N$lmiDi?04lJSx9I6(8WO)uX`eNy=et1tNbJtbq!OQgN=>L&=n zgRH>Xy9)iI91MJ={8K(XaEOshd|I~bK0`*~>WEK3n0zp+bNIhF=W!I_gyp4)bM1n- zcmFoDS%^p1;r}Wt1MM+Zj0k=Yz~<4uGUGII0^FxBuVhSQAQ}Kh8aD1TX$D_(&qM|SHH;7mG90i*ws0Q^WduP1HW>8C9K;5wzX^pv zy$P#;oNg2n1gQzllcCg|5xNjnH$1Jqae2KzD{D<63ZE33%4pvoAg&I63)U#kIT)6S zWdv@~O#>!C?h7D)4;LV}m>^iNWDAG`A!CC+AYKR({(_9RuzW}cRr=o#r}>|FOZ5V{ zgMS}&IaXGX3qVLmISQc=$-nRL?=y%KpcyigC?j%6CVmuM7y-CBEIEVE!;YiWWlVc; z?fb0!g~a@Y^C)w_C6v739S0iMTSdv3Km78SGdK(59}w2w02HDsn}axjJYwNWwkQ@3 ziwL(3rl_;)umPAGYYfU1KG^#2PRf?DjSLtcV>XtPD9&;ksieUe8QllsZKP)#TAEN& z$HG!Q@lq$w%Boa<>;cFN@<&{&22wvArqZHT(ER48pEsc$WnbB?X1^P>x!<^#OxZ!KY#w*P70rg=KRP@w$GbNrr@l!g?dUxzSfy9ucE@+x1SaHaSJf; zA|e^+{odR`A$GsszLWMH4G!DZuV!%RuPSul~|K5+L8Z-oNxu)fB*{ z7=lDEBz4EeZrnPx8_3@cq~B=atROrl2lfaS>ov?xA5DO$61?wN>Luiwm>WP?QjUBG z)&FlAu@1CLLuMFfzd_1)CkHx69!i+3q7>_b8a@&Ftq+09eAz%s={WFSn z|As|==!AGL@-9IG%*Sj%d@J$l`*~Oa6iXy^5u)L&NHdRYFPbp4IrrZ`prR+?ZVw=d zhiye+ai~f_g!~>#02l#6qMb#r9tAnSbqd0LRtXCyc-ibmi|mUFz(?TLO;*f)4zefP+*I8AfvigD=kr zjiH}Y5cV=O3K?!dh6^!QLAt$QKSY3()C_(rqG_q|vVGx^t;eI_v!2E6#O;G3Z5*rFoHHO?*7H#uBzeXw)kG8}o8zx2|1`H3^J zPVbZ8FSG2_8LD8P#y!CF?A@k3y@1i;Vpnw(9}Z=_Z&Q?zsj(xJvX9k zSUIaOy}TCMSA4goq@=ba?A-N(pWdm7kPclckj}m(V|jmTYi07iY5F^Xz2&t7AzN+= z?%ixWa@=Lh%+(b>`HxJQTZXVYl}lpLNjeF8O`ledgh*bxBYoXN-^BNneapl97kaYt z^1gik^5v-YL|tERn$?FB%}a+#J;MQdn1HK(rC!2=uZS>EGmh`+Y!0L(aQykqjKS8#(H3o?p4?FVp|0861;O^Z$?_gqg;#teyun{;4@MZPdg>O+OmL#enHN7pFqsJ@#Iu_Od zX9j;ZycZrD(p!fxo^~YsM_;&!z}-0d9jQds7L|YuKL}4ws}9n~eMup^tJ7MA|K2=W zz%~`-L3o4EymZg4W$+EIZjh)1{&roaW}nagZ3P*|u$F`K#($FKyS{UZLE4vlTKvbJ zbFg~nzuhTl>%UQ zM|)C;WOb-AV@=@G?!F*B&bsMb*fqy46;jA>R4@9g>j1lnU`sxd;f>Jl5+%3>;RUn| z4BRvjA4@C1b-4I9>bZb~3M7y?CiQ93J$f zR&?|@Iw~%+;0nu{(o7?Z5thC5X=M^0i=v^Iw>Nae_@V`lJO+=T)1y)i8NE^|*v}!} z!MZ#?mgyzlK?oUdf=f$L{>dP$YSDm~9%g#UsTwC^nGV)M5lBJZ_P_hi*nD_-oKDpM zHm*rnGI1wcbumbia`s{3QES2K;z^BdBrPU+<40)JRPGv#JhT46+ZD1UM>DjNS(9nm zp7X=iXlaH~gFe0EFZg3;}L3cRCW?G)AObZTz zaNoW3;Uub!gCe=9@89={@NVV3C}5y#lE?)$Z13Um{G6Zz2(7O1$91~a)&&^=zF5n2 z{lgd#2)pVm=;4qex@W_Nqe(m`k0O03_VSj!KfylDjKRrTeXAW&HqbX z_qpXo_bcegn5u^lx&8{Z7#sG-d1DM*2fYg*1n%FLhfUydK^#+Cp&%YzPstS?5cU#x zYqOqc*FL080c7kDq55Ga*0;K&GD)*frVI1;Z(gOIcIW&cL0Tp>&JhZ=e-|I|`@zdU zok}K?nPBwx_w{1S2WF0v_LV|)ps!t~;=zMGuU-krNa9VPjF^HJA-s`3p6en!Sckzm znqojlr5Ir1TU|T`V1@-%K#CoZ=@&_-WC$f2qD=qMy>&Wg{Zqyy+C!%t1p91ff4#mn z*tswqDS$W0@I*7TCx4%t;^lz7t$IEJeR^hx55tCO-@_`x`_&18H2dAXi>{AYtC{gF zz=uwiHH^dC5EkF7=$Jcy_w7LbSy$zlf>np}qDer#n-1XvO83fSqr?9|bJu1^!99)jE z#bt(=vUZh#I6pm3DCBYv0|wpeyjA=Pdr|uQ+-mAbU|aGuXH?Shrn$nhB?h`!jur zGs@^iBuw|y17T89RyHVkeJc~+tg;EI0&uc(i>~o?@TIEy*>we4`RQQ%ME73g8^rIqXl(iHNzd8aBA6+lQ69fqv!HbHEK3h^*( zkukd6;(#S*rZr5+cQ|-eB|x{$;TMd6mq6 z13hb;pA@JAsCpzF%tv)-8QNVMKzmpgCxx*lb3oLN{WmI=}HxEE> zrTo@p!{x!X`M};NLhk$IAY%Txq_X897}PPmc2H|=S5&hEbd}DBFSK;`^;HE7Yaz4d zSzQdaPZ<%H*RVgzxK{{iLGR7bZY^}dCr&X1u(=40;9)1!6%BuC8i9>_g;kExh_Q3| zmJVmr$3vs-uu1^F*Ws+*i?R!Rt2UgjQXtjWd!!T%;S+Ec;39~>fPBIfPHf~eAOLV7 z%j(b?9Fo}uP&0<%-A(!&T#E|(u0%|^-zzH%g2EOXTieB@sI}=Ja7#{hC2l(H?X6zl zf4lL$yZfl-*Ah81R5p!XFaLe8{qgyA9hE$Tbvtm&)?$f#ESnG|A|Oct1@L7csoxMU zZdtS(NDn+dJT-%A00zAF1$o_ir5(;6g`KUT0Bq zyF1W^=`DYCp=bUOXN-Aa3rrz-xccX-oxQ!Af8-8gbxn=>**SSQ@!Eaq#bshZ9p~*> zcooXpqI1Vl)#{P8Uq^zVp9^94Mi%Hg0mDkUdzU^iAcl;CJ8TxjEQjXDt*i|=nZb7g zvXF_Df@{lnfqZ+|8G?avRAd)ny$(kKKkGOW@#ScC9oNUhfBw8_9Ab*3JwLHLP4cQj zGWu-t^wU`p5Db{mxs`_lHHE>veMKcPwe-Pdo^NOC0B^j9WkE0<&{R5^zq$itR zJ#pl)r7-VBk5F^Ym`Zt>cK1i`_gv)N{&D6565sR*b?t6shw6^QbSE@H`eRcJtNh-* z5{V_CI7nTb5;3odGr_^Zz>_(}o)J|h8H~^^34kUuGn3vJrTy~d z%l+Ahv5KJhE0OunPw1N@-o5)rh7y&9|8v#TPV4MM?pdiJrXNz!Os7BMPFlSp-=%eJ zLY-=Jd2-SRR^3fa1i5smSs2Z=sH&{gD|>SrhVfQ_gNv5bI8WfKO*?Sb{dr%*{G9e^~5nE|`< z^|ae3#B=TXbr9!O*~*@5VAV)CC?6R#ewD zK0Xf0iuMpyf1#s#Z%G%8g_`G0oIbGK|KZr?@AdQlhp8_Qq;mbjJ#`w?sSKrPG!{}} zi$s(p6eaCSDMhyGNQSbhaEe0hG9=lVPK44RBpjqcR5mIVLdw{{E|e+5eb%Snz4wpP zQOW+k_g(K=&-1L|-9qBkPkqGUTd8<${ z=ak)WLbYV5Shd7QrM%077wVB~9^vrXsf(_7c(TMN6bZ;k8h8rNU^Q***0buJ7cd%c zAitoe2Uh_9L5IfP-Vc4M7<~r7@#B{_bCU53e57+$$)iW3sU6G9i%(+++dW_gHu!u7 zlWte&xD<*70Z+iWv5p9LFtwZpUvT=dEVXbH+cO4*Ux@SJm*?qMe}6^5#fZ(WnorRR zBTx(_0PVyLW&Br);B^4uY4di-yt1tKAJ18)$ zCPo5c0rYX*G@cWcWhlX5eGc}JFc5>9Q&PanMjr0*#}Hs>mt}jG&DNNCPHIX3zJbkN z!)D_x*lfIoSx~qHn@u59S&`1+c99blf+&FD*_a3X{ge!n#4pTmjT0W$*3O1`<0$h& zjT2TyIM8{9KWTs^nd0%lKnNut^pUD+xrn!?P>%!I*49Q=PK9E)akBdT`xZ0~z#9VE z_^*s@1rr0Eu?CU=RyuWREIO*jrKB`aWg|`90aUhGRAiD>(_9!dRj>Dh-+-WeU=nbM ze2E6wfsX;!vcY+jOKUZ`Hk759%k#4jdNQqf+$r#}7@} zF);2nP(pB2sRSoIA*NE6`7Nx0Y=oP8iHCXD~3(AfTfXvuCYEZ zeXf{id~8yH{OQLhX@G_^jjR1%iH{ zSv3wE&S`iW9AJb4GJY_e^;IoEZkfO~WGA?QnXnPdivYK22%%?@u=Z*+aOPt)$ui)5)O%DBq4tzBJe8t=>~yaV^jI_vp3 zXR7NC0OYb*k6*o#!1-;0We7@3!5=^nnAq;Kv8v@K7RSMwTre#)8iMQl-)1d?$AU>R zXBo@M3$3oU#-mm{y>ikDQV(mzE?7k<;yaw2o!wBpy4WWB0Z<_Isy@7x_5S&x!MBIf z?%%htoc8)m{J)i7@pakr=M$;*k9rX>$V%TZw{E`w18}I!y1JdWm+kvCmCYVh|GsEf zfT!R6?#$G>*ZVYw6w141B_}1}zOYp|i!Z(*=M6lZ_r2-DhF4#IUV9=Hq@JFg z{j{M$Zr!?d19b(zxP6fC9z46SAjjq7XvD{t`wE4^#RyLEN)&fBXNFSTg+_y80d|9R zs{dB;tp|P&ENfBG)6Px}>Yt+pONw%Cz+fGONERB3@W4eT2`^Vd(^9w}x5>=u&>{D} z&iQml25!L!E4j2J?XIr@D}!Z|bvN$Glfhb-@8hPov`$zhbD~dcf>X$YmfT4qVA z0TPW!Sy{P=A^|K9p&j1<0t#sYc%!W*D;Pc_Ju9oYV5oBVGFWCtTG}e0hRUx%Som?| zTv~mHTKjNWA-nrq{f5}q*W>&~<>uz{iW`UaY4)!LKye2$xm+32HLlrfC{49w$`dK5 zn8q-(w49u0B$Rhh2@_R{$15RV6IxD8R0N{R$jHFk`qO7G2Wg5n@QvNhfHt!rfUc6T z?a39giG{n>{;mIJwXh^iul!k8G7Quul^Z3;P62fQa0M8Wah81@hev zh_D!|=HB~#54m?htZm!2?e_7B#k(7dyjJKiu5M`$11>)Y+zfxX&6X}Q-`;854dJJut1AfEWdc?yS-fQ%7TK`saa2OexAHvj7(~r*>h*|xW7zVmP(6N{ex~~EKLCC8E0lbsC38SR zChd+|`J+dA^|B9ug_+wXwX_y@^j?~0uQ}KK`v+MJmEug9v)qU$ZkjjMsmmFvALDmi zQ8K(3y98^^+qcQrga-u~ADdB*d7QW(*y5gw_x)0nnVE zU)tTRRZ~*~H=T~(>{i>&&~BVZxOPA&gkvYI>yyX*iGVL*kKb-wc;+N*6{njfaoGp2 zFusNFY4-rmCKf?-Z<3 zZZ(*~*|TRgcbMNXeYP^5_7d&EX}#G&7| z+#S=im(MSh96gI*baTmyqnDO+71|DrMRB#Fb6$rrO~wNxdIp~5F9ul-F*sgn8CN(P zGhJ3TJnk7#>UsYwLBP#ru&Rt&wEr)FP6fOMR!1xX+fkDe zGn64Opjc$WY>k?MK{=o#0yv`f%YB*vCI|zbdLxJtI0JF1YpOxVRC0*;8w>(ia=%xO zn*XW!dxOECCRRePyUM=ScX&`$RTV$KL?bCF3G#7Ditu@Qo7(rKyH=OY3UR3Zs8?>V z3^6X@<-aK^(4c_cAqKvNx1&jp7yxQe96Ya8+qX*_J=K`r{;j#Y=8z+od&#Gu++`V1 zxSFo+x~!VSm-{v%fkIT^2}Jv4v(ra#5KZ4XbO=!vT=>V)6>$16N|+#I9L5CUHF5G{ zTj3*PTMMPGUvw}fp!Te&XdD{3^-!kwdgPqK@4LY};;a`UPXFrp)(We{VPy0_A^`-k z-yn+=mGwyL*s){Ly4>>%;s*Y@ckgKJ@qzh1UmAVSefEf2CV9O&NoAdr-$Sh;$~&Z; zyMWPr#DB&}1R*k|e-O#YKb5?TIAB-;Y4${LvlfndkeE&UYI12L(#gt?w zjGA~t%xL3Eu&imLWdp@DPWmfc{pX~}2*wl{%j7v^ckkH~>-}qb_RqpwlRU;B|H*gs zM3o0d#x235wRtVL+I=s?V2EpyHQ;tITg{ni@GP)*YG1)-(KIE@F2F4a@P^xYp;Ged z#>-=-zzHHqu4hhwY>`Q9E11nDvk|5VDQJXbjzoEUH%8&w18gJq!(1V3jxeaJ;Z=2Y z4xrXxszBPH_xbrNXzj9Uj(423Z34@10FON;%n6{jP!a~<>Y;;CYLMEjv)&(Fc<*}3 z8L`p$$aGcFQCzwLt3Dg8VpCJ)T8|(l0j~oz z-w(4ZYD-knM1(Se}!AtQ2)n9!+ zOd*IIv;Of&LJpY73wnY$kce}2Ad{T4s%aJEi|Xp?k927vs`-HhYY(>>54HIAoyA$J zPD)XMA;E%h3TZ*_-J`OqnwlMLKe(Wwn6B-hsamyg;+a*eEGH?Q5r;3EHkSAjyw=tA zxN0z6l@9DJ)jjv7T5q!foCX|wXcXIOgESx*$kcyATAM~0zE?QZqS-ZH9R%VPFSLpW zQ-GV3lt0Julag=0u8f@ybUm}hNg`^|<+L?7B=K$gw2-mH=}uRbFL#GD@bCNyvSKE3 zMbn{{(OVA;ln>9Q1390ubmY&Vm_AR(C29|kN5z>kHsRj!U<)H$T2;7ysuXI^ms=CFU#Rry%`p!B9(Gmh?@ z>6b2jI_R99Q@5a1w^PqCp5NM38Z8D=_dkZj;5nzYNp@G?oLO(4i3|t)oEn(ez<{26 z-D0bDEhfteaad+-x>#gX6t3RXFAA>v?YUR{oB^MADI2}>If(ImK3~@V!QW}fSR>bq zG=)P{O3J66U&J#rGoR2+tJc;`TO*2?fQI2`VI@Iy@**x>`pwtCnqi?OUC~nVFm18X zl)Aba9;i-}Hm6UY4i7~U8URND+#5$6N{_g`0>lI4LSZ2|Z;WDMQc#bGMu~W$ZZO?6 zcz94dx+xq)eYv_ZbL9^vN1jwI$;OC0#0%hZB7le!NXJKYc&>*d*s!7a;ck~MPrcZ2 z9-fjqsgFGtmUkuG+OlwiGW=*&lIl7@$;ZqOq^UNz7Ua>1KgZ+zH%IJnzOrT7*x@g> zg9|a1W{XzX0?ZMgf97R5m4Q)+_=)SaU7yC3N}$I|m>BgJh%cu_zzQjnkofzgjZ! z0OLBQ5+EMvD|q;>?+A>=0NUpJ+cYL3ZlHHV7sdgJdNpjA_wm*lbq!aH$+P3ZUD^q( z@B8o@*9N-|L`3QMbh)0Ny`sp5d!8ghv1ul<;tDgpaDnrnRF>}tk1ZQ+yE(V(0Vl5M z{$|^N>tmo5B7;wfeuVmix8Hp0sebpap|iOJz!)@kNm_dPBS_2CYzf#7r3*UNb_R*@ zLM@V4gFc>#|A6&l2mvn?^4sqt1FB2%5Q~M||Khcfhd*bGBLpi0F@)paWQHIK-iyNu z0u1v3B*9M~(IfDKtt03RBdvr~+^HG6=z=@iY63iX>5=7P#E2=gKkyx65jcS|0UI+lQ@Dn{~K_@vAVGx4`=FCy9cFw=w=RL_`o8Ri3PBQ4wpB*1|oiE zTSj?$dgl>+&&n7Ob{;m0Dl#nQ>x3f7i--R?qr3}ue_dRH0i^)Rnm3Xw@kuyHy$gn$ zeBmQS_6TGQUik%ShChT_FTZ|jGm z^nU%61W5~0-$d!1@5anOB~_}*!OJz&4~dF%#pm5xaed^=S%>U=_QY~B(A*Y!iWlNw zay&}mfY#Q??Ib&o)nc1%c6id=J)Nq%s#Ay7jxdS8iH+U64^Kl90uIYfto4$iS4+NM zOl8=RlDhHj>Gn;fc$37$HNv3}F?vJIsbFnEBaa`i^uLogZ?GfSZSaJH{!iWElRw`a z@$@()HBY|i>BD9RO!V;Z!B8l7JZRyS#{ofBR**O)WJ^NUI3B%B!^LZTejU7d+vft- zZp?xW`sIT?zB~{?I#(q~Py#A&Fn5=7{vd!ZNGfBr9xfmq4IIeqCjt^qJ_-21E&H-+ zn!L&oWR)f|X7E1+1iBVf7T7~%TwHQZ`zvR{QgS64K$;NhXHS9C#gRcu6<87Q5FQA0 z7`QDAp$PgQm@NY=Uo%NmAW1l0BCUvK>PweCp*uHG(@wKNe@jp>vP=X?Yj{2wabt0C z2E;_LcRcjVec+UU-WcofG`(&2jW_Sc-|SFv+qE{MI`7KMebM_+NPe4Aw6HM55u9sK z5~M|DkOqeA(A;1hkn`aW^9KH;;cW96MFN0?hU3zeiG88-`o!TX=sr9wUQ)$pmk$p{ z;lY7IkdX*3X5?2&{!tloey%0)q#irAPWGaW<-NJu#x)SJtannWm$ zBwAPxJThJx<01Z4nuxPo@%c`6Hjzvw(+6pk(XF75BHTfiTM+9>=z$baf|j}-ydBOt zr-=7AH#;y96rzuH1mXV5``xOTx$L0dA3CqGpf7cJp`pk>N+_zKtd8Y0r=2L8D9Cwk z?>nT3;UE)YJ^QV3p5%&5c%eaqXMa6QUx}mzk%W=Bic%R1>xlJWe)z|)&#ZdrsmXEA zSPN%Nc?JA!*;jy61_W<6Q?yc-5P=dxGJVb#oY|4%G2%5d{l<%S+78r~cl`404H$^> z{dEj<*}bQ|*z3cBxlQst_2r@RYq5o!ugHZPPLvTFJxXgFJm#Zk_;4f~j`x)hM`M)2 zaO1P$I8J_LBY^)v!;&G@g|@aIgOZ2mAfVC*k9Y~UTa1NE$)7WxI?Ij+vI=w?+nNXH zj@$yGUQKNq6eXpJ^MOfV_W;MQM+AEO9V8cI08fRu_r8!tjDekesJAMXN)*fxpnvPvgfpoqSloJt#L=cPGqYG_&8|05;BtQ!df(PBFt0R!@ zI%f=c1HVmU5!$LV=%65}LVjh0pxt;Pg+d$nRz{MTIwFX{?tt=?CIZ!X#b!XTq(m!( zNbo5*b&@3@{FHL0nqCb@-VEmrWTvXN7J0n`luU)I2Pj;P-DfC2?J1Lzgs_GKy-yPb zldw_5%eUWpbRgG2;Lt@b85qtOSMcyi-Ov1Q^?$dg-n;*9`<94{bJi>~ixn<_7P)~CTDfy;=dcn0;_G9FrzpdjTSti78=;~m zTX&763v_w6-NX8Gz1u2~&8>~Gv-c}4H3#GUj09m_kh&cwQhMYcz-g!(E4*9~l$rS; z$!MEx@&*avdH%~w4$|KA4Ocgy`pKvrH!!WJ#~qZ{k1U@v_uJvf6|CZl4doqQ=MC6K zMn?XtQ>$Pq5vCw3d&1uro}8&++Irh&Pvfg`!-J{%B!WJ2TObqkuFxn-dv+CcXgT5e z+(u!x?ow9cQ9GVV{00e9^7F+)3T3#(0yWkF&gop_*~vkmn5$LjFSe~<+a1}D!q=#2 z5k=ip9Yn%dy`WtAp!<5|>MVFfguOJ?1T7@C7Zn9kt+BsXk#TBMkI%Q^4WNQaFMgL04hXHW4>P($FmGM-yo{i_=mvBJ@Vng%I8PBS!q#7F=YY$hLM@pN95f8n%a4XV5IGMjKD>W6tv! ziBSPXgrXl@cePVkk}?jMlNOHJUhAUUJ9ELA=9^pD@o3-(QsO9{100H1g3J@4n7>QZ z^fhFR5{>x_76hr|M@?IELV=_jKL25zE4MZV)iA=`p1H@B9QF*=w9PIEoulia_S$LF zMadgC=G^~zF0~7l9JmB#!=2g*CJhD!J*X8EF*Sc^*YMY-eCG~$~8|*wj~lQILY@{7?c+q^oan$UCy;Sn{N`P z0N>Yu(}B{OT$HUN=p3q#iPm4Smb}nI53g8z&f(>)P!b>m3GkArY5W&zmur*o@$rV# zc?gE3aFvs|$&&O4)vrK~CCu|{{-HfSL%N865~e8e$+cp!?0AvV z$s5XDg|!;i5qscMmU5HlqZ-RAwks)NZST{y21RbI6VAuzJ$bqMSI)Ql^BP{%9kQ!@ zJ#97TH6UwD>vEg_q&ypUj_-ZG)v-6hpkE%I5@olwb4FH5wZw%h;BDuLvmOh8Ga{wo zT$%DN7(EV;u1DMM!-vD+$Wf1*<913)o)V+=XgUaCiWE$d&x}Y?27q9(UTG#G_3oS{ zEiH{IYor2j7%S7cdr_o-C6s5;=+>p1f4csU5J&9O>0oOe%Xa$ix;2A zA%MvTAtuH8=h}1d5G*)U<5Cjo~3vVU_oITs=HXer1@#>oA2ADe+r6;wK*6 zdemmyu@eTvpNbipAhf7IkX52x(5G2avJeYQ2MSS?sy?BL2fY7jQ5MYy^=c5}py25u zrSZ2N!$Vo3;`W93{C-+`hU}uqX@|3yB*!?y0h@_;1(SR%YGBCx*4Q?}p-@Vl1+%0g zb~wMsf@^%~rK|+l9*)`)!u^-|AB83juM&~JDXt4Gyin{IxngPYik{;ahkw``6n*`+ z+{m>C=dO2ws$I#K6Q6c^^a- zq*`pWunc4YQ>Cf63evwjm(mhhHIdRacq4A>if>+`U|%%pm823hbdY(&exW#0%LDk= zD#SNa37nd0r%oEL1D6W8hMJa33KLAjG{Y3CmuhNiPMT(O%5~2E%iF^gP!2O09p`NJ z4>tP=N{MK4^1S_*H(}zVg6SjhuBC7{pQECqn^K4SQ@y&Y9dX%h-&8PXIBWzHoCIXu zR2MB0H{2ZpxWXWjN$)cMo?-A~wE^fA{z^6ogpTIW$Ke|o8~}U3MvyI7Gm!d_ScgoOuBy($f@4+y;ZiIoJ{XJ6z(52|H)J*x(tr-Y zKXKC6&hri*U*9KlX!+6bb+ays$lmeTbM9=t=<%Moq0n-@?yTe8*QE~#EzBi^WgU$R zl+KJceGRNg$21g<4QIUQDZmS%zRx$^h6Yr;!wAqA$MIh}XMH{kYzWW7urlShe}Qn9 z3#!Tlf$9>559$!lIk|K+D2|+_SB`Kc*K>4*769LP90of^T-pko zTO+~zVf)BA9mn_&Fr)und3=(M$~LFTkHHH|JO zDVQ3Ld@D9x(bN!`YHaW0$B%KQpyh~-CS>H~1gQxul9KQvKy!ku3z*FN?t57sR7Hp} z=^uoXzIfzfD@`Yb8P!8y!2!+t!cMibvpcyR3A8MquUX5E_o~s*pc0yYifqpZ%+ncY z+AyR5+k$I9K%?)3C@G^Dm97e$hsLJKil*s*?He@%sIdR?gVKM}Mra;SoJ+T$0Ttl^ zaR>?hA~Y8j7gy&W(?+i=mGXfILm&dkJKhylkDUYuvpJV*b-_QkqE!eLSHJ_Aic`3E z!H6n9V=Ts_%m5sTA}BPeN5rv4a#+<)cATsG&+uU;i1C`{>Im2lI1Kpb2`^>LtzKz9 z5g>^PwvK~>#RCKT{#|YVW#DgI4m+x-GiI-@M@Xw~&UNppty|08kG9;Hgw(|U%$rvnyJp-fhdJ5sVeIhPalhlfDyuFSpw`;EI??Jo<=`xS;2p1meWk) zTYw_fjmKfhOe3Ye@foiw+i$0J+62TajZmdeIDzp>IG%K@5kAV@as9CAfp(8sBPU); z-a9P2H8x>=`@1JoH|V2V2=)&07=v0|zEv{HaPhXLKS~9Q+*+?+&_l?0wkYep*{`Ii zr|EMhiR$Wls9MdMH?Q^dEa%;O_rlUo&>m4S0vr>=ZJ=2|ycnS^j3DznxJ4+c8w2D) z1(cEpU_dD&lL&z3@>;>N1eklR#MVaPj~rkaHhZN0!2f}V(4w>sBsI46$E}+;k&1dg zS-Td+Jrjjx7{=^D!#&|rLyeFYt&M^8QPm`_Yrq+NS6DdaSa%!reO@TzUJ(G`LBkXj zOx^6m6r?M6+pG;!c>G{Yh?^w22IRF^>gua(6JCD6@L}j)DEFolgYkMTuM$Zw97kNm z9q;HNtTjD|f*>jRC5j~)*bJB`N)@TjKDL$i$RxhJ4xVQzv*p|IhCkZhWu67O#{g}D z++;lJ#{FjiY7n-8S^NY%5IHn`00a=T!4iRO8-7@4J1{!x(%YQ4jLP8cQT9K zVU6M`a^@aFH1rC~-rTLxQI9Z*AAvsVN<~kRZt#kQp^q#)h!GLR;U6z_J^KY*)E*Rn zf_1^7UAU+KG4O#@M$!oLRAKmvi(;o46nkG^w>A_hl!6J5Gi~^qY?1cgyF`NXguJqK zY0pndQ$#hC^-k~D>I!!}BK)=msgvT^ zY$kG`+4Enq(YAe^TSL*(7zXQ|4UGo#WvN2cy}ON$ksO*u=y-74oh3LHlnF-wb|<;$22FY5`zlo%0O*pzD0%ZG`$(O4wT$ZF{>nWTbnMUc*n} zW!Bxhk34@F3#$eXz>MId_x2rY-M0K!u!$X{Yk&?MYI$=jiW8{&F|pf-)THSn(vU90 zcK~z~NE6oOx^e*$;Hqn5z?g3XcM)eXMdSx>2T!GAIF2t}8OEq6p$Ze=A>r;bQTeR@ z(Cx8I zG9PDZr)z2xoLg}K@D|M_?p$aq?X@vqRo0!JseCI!o``aFnIOa;*!eX+hzlhZE6Wwu zJr;fM?tiga5~X`Cfbqbs0%mKPnx08Bn*nmpRM_L7Km{58HQZg$+1zN`jKY?Lnsy@T zI4GO~uge(G6FB4TVyG@-*=wGhO*xZ(Guyj=O00g@K}50F2x}CjaxjzB^V37__n-{} zlNUI0Ldf&LND}4;H7>AVIx8w>P;^MHSS3NVROSJquf7H)$1XR;>v*eR_KortCB~L_ zX1)!jLMUdp3?1Tc-u9Fm4GxD1n@jus?Bk7;$bSWJEn0l zbs1wPI3P5i_Pdj^N9}W;Ty$hJd+E5g zCR;Ry;BNqitRwP-M^f!y;F1R@;%HT&_;U+IypIhA9wR~4jz{fy8SE2I&IQ*UPHwA8 z^TQR+alfZcGdqm6oX%}^^l5SQ{CUH+G(i7{ERq&76Yy4*C|AIPqg7uk3abji7a15Z z8E969D6d0`Xa%V*5U1eASCP43S&@Udeq_sXo}>9ui8X0ueHYykk&24kV3>%Pz%y1C z3g6%e+iN$05mzjj03dDnq5f}#_%~hUX>WJR`#+H3wkBrl)|?XS6&l1OD!u*u7n$}w z^u8lxP!pqr^v6eC)1EBH+4urXLJ8lcPRIoNjQfIh!;$}(7kU!)T5~#kGQoqepqb@R0FfwOnsI&9>HVks0X-RV#f zA~!_}+F#6tdt0&G9fk=t_aj|A0+AISk8<2|xV4lJ3-eUQQ-WoeyHmAEe$V@oU-iR2 z%s_Rq(Ul}LbBrl?atnUdk<|IREY6@Dc#OE%w`29cJnO#hDh@Ch*fY*F9=I8L6&Cn{ z!i3EHV?7$`>JI_)Ov51m{f{_#hWf6--MHWL@PAD}GL+o0O^mWGU`3HJRJCeL&cURyI7Oebeo+yJL6fg`vKZg_}=z<-vyST)j2u`&?DD(h&m0K zEn7-%*_#^hzjfCg6Im0yikKZb8*fxEU3w~OJ8W_oOH`s-{D|h&h`d!;lcG=tG z!oAnEc;oZs%z}4yuFk0dp6}{EZLuvqC$*N!w;xjj$AXE0!aI>L5xM8 zTB{DyQcSND)8T>!sj(Ju`sWC60zBYXVcDloN{FT85g<6w#msbBBus=lq}=0v%|_+W z0Cq_Wt{AZ)*1Ovp@}=Rw9vG-K)N*L(G~G+nR+|Hc0qOw=2*+s}rgn|gw~uR9KNv$O z(K@2>fykeXIv1v}$I~;CE?-4ay4ygntAZ=+mYFw?=z;N5iI{!ylwHI9mDE2C=;6XuNQ)WCeds`;goFcI1oEkoT(dIYWTE-z+}^_Vx&;!|Gtb3IQxCzoWJ$eEw(=r#w+j-!8g0}>egmEphHo}q7?XOM~0 zD}dX=f0HuZ*Ee=(D6JT+8%RJ=PRrY1Fm_nL?;A@s`?0uQE+g;o6nrxxC$Mmzx^;sS#;o#A~yELtMCs7C?u{VUaVWdoxf~;Rmv@vL-#uX!cVO>+;6+6S(i%h&{|VYm3fe}p-hoxOh4XtXLMzZp#Q?E$|y|&ZJj9Z9B%TP18Mi}6`?+!0tEII+d>LB>Mr#4%iiZ7n;`og z$5b{5p12dUq(V^I0ceH@+aMh14IW-bMNqz-3w#YlPQWBKXa}NlcYCR;iIv&Ezm5k~ z&doc=)Rr0U&J`Q&T=dg~s);WFg{7z84OFu}RS@5pg@$jbD*JVDR}7EzL=VV`#FXcsg5kO&O~AHM`?g12vZG!z9OLKt~J z=8i{fx@w=JgXxvIG0p*hBQq7GZ%UcOGVc8gw!boM8%2k|i!w0x9%XG!2fm~Zsi6G! zBLby9WcQU4YAE`WR=J)QmGgTc`!J_kZ^w)C=f-I4l6`1B&*i#FF5VCy4*|X zE0CbhV?x4sg?@OHV0zSkWNaCYxya-O*3sOTwJJ&pAC!c~l@@x>_$MTZhA0n&FV)b9TO_64stGo5%d;K*qKl81I4 zT1bihII?_aZOqm)3P;n7B$haMILBUskduD%L!!UfvZYxjA}4MTVT;jCP7V~uW0p5m zPr0I$J=e5w*Hu+{@V8AS*Av6l4X4T6POh1(x_A+JW=gEFLLCLC|53QD2-B@be$`Xi z((ccY!k;D$g#y(j0Y_IL{QI7UUtYyPJeb~+a(9mwo%)@<)58@rC6sXGVXq^{81#!F z^VqxNEv&eYR47Pq$&ZS9TnE^i4de%&g8xwk47BkpDJn>GA_0H@1ZDRCT&cr|!Y&02 zCb9T=2l|?Ez`hc#gn0_fP+`Kfe3NyJ;I>%Y1cOQ~NKDY`-gic1-GP77ANVMWI>fcH zt<*_em&;{oXh1V(v=f^r=T`n1=wwepQ8!FsKj1rn{EwwGCmXL_>s1l;z7X1jx-n$* zK3sxWmWm5hT)@!L1{ymmvYzHvw5=>L(|7R58G}!F&yPR)^2Rl zV`Rs0R)1)&QkuByJW2{nt|*0cm`y}p`gRd=gd?HTX+aSBHV{jZ5c$#0kCijsy<*G( z#U(21&K$wkEhq})r^R5y0QOyhQ+z&O@(HT%i>cp(+&2I{`ibsA{73!*IRPpR%FIMp zJfwvCKW`oCNL=C_gs(9L;`s4sKn$E8rHQwnP=XSXliRwTR+LydW$&AwioYM~&dT_^ zaxr9V=&1lLFArpTVpKwiQJjJuudTiy4(VyCc&7Wq8-?)yD+X{Ok_-}v;83oM2)n!~ zQ!qXK&r>ZXI@#kmVh8#Wq2OGEWlc>A=%+GRbva3IsJD^);0)i683R8y3~N$bRU8_e z5sX10ZHGE9Vch1VHI|T!ps<97ibH67h5{vHAX6xz;ABWgyjp-sau^K;--n&NjfQ8K z9LgrLNriidB7|jLoeJJ?IQK@hBv&qKL417X!lL{X_HdjWIj_&i-I3k%cC^`nlKK@T zUw0LV`o3n=CyW>2A_`?fR{HUM&-|id&o51A616EumeOSr1UGVH3hLLC#vJxilspw8 zmm03QRB4@=nUdANO}60!e~pLX4wQeIJ~s8Lvvx>MMoezkCU`p*E5qh2oxuD;4*G1S(5Bc<9(H-yW`SG183!Y#Q+~%4bu94B;2<2Uj>}A@M-^DM~7zJ{6&Bl=+d7Bb6SgxcIGyGG%{^GD;NvcWD8R7`Y0( zx+%4#!Bi{0{)FG>!1$qw8H$Vq>E9Ruc!uG-kFYFAx&DwmiJbGz=BxY7+WX2T9$MD_ zXB$73;7JNhAdn4$wgn9l{aGG(fiK>~x?W}^~fJpw|+6*dcKz>)#+_WMb3`i>q zDHp_>|?w18*LOEr<1jLs9$P^%4bGX z35)L%V)*JPZ~-+5+qPq2q*S@-35~I)PP_hl8p9Pjz-Sommt65N(Ij5Uh*Pw1jYQhk z7f#E;sqsmGnmTv5qF-l)E(nhKa1>7E9Od=o$A8zBUT^ufSP!kqQ|{kYVtRcB2?FHwo7KEQb9 ziVCZ{#?g1)CZ?*cdwX_t(BEJu@RYmG5cm>cqKYT3j9msEic|f1WCxJ?&cq!?6Nz}C z6(7M&MV)}x|e{hL>WRtwmp^AqRidw} z;*U-w#F;>TyI25pFdmgP%{#vxodG9M?mN^KjRtNh`GTRZ1ytwKS5>TE1OQ6C4d1-V zk5M@fP%nbW0rZ6_n37STG~nw`q=wQ}(F2G6M?}(>`)DW|rd}*VCkMm=1oF#CCh?3i z3`D3u!Zy$lL!~U01w%)u(FvUi=v>3oA)OTlO`NJum|h_AOHd}Lz=Hx7j0esMg@?IQ z?VAq#w`SE6<5!#ucbnEI)^#mxd!G?SbV!B@v|4rA^&E35uFw?Cagu9R3EXy>s-Kij zl`{RIH70cH`iW99L0`?hEGLGqd|qJcWz93WFgMI|P2q5yR<_squX>_+gSl}&{U31i zjp+6HuTRQMjNf-TX`lD2bp1KjWeREWn>Vp`HQRodV`rGKwZ3UY-OP9c!rzVUX8sb%B++F+f$ye z!d|}l`g0?j(o%dgWweh7_K|?iR5Mc5!+t=*W7kxWx)Hxp|E&ux(H&oIq}p+=sic^d zKq&D*+cs|tj~c?MjAtdH39e5|P+hlOcs?-1eSWLp=hsS<{iApd!Y=kuPSbpK89D{$ zY%)VR95a3=CB?J%V?bYB{oqd2i_-04ao-7$Ab|{MQ=B^VCDjd54xUwM;&!=5stBq3 zo1j)$ynkj=ZA?Vq2Vo#8k#2y*-o0Chn?;|3V59mU_b~4u z-FAs@UrAnX!&%e>V`NQrRn-T&r``Aac;5#gV&!mMLXfU0%8HCuhC1J_g>}x6x*6}HD%yB4B?feDS1P^yP z=}Jv|d$t_3R?IEI6C~Ry&6CrmX%A<#FLV;ku8|ScF2OW z+*vtktz_|dZ=d;NZrj`!Xnawt0PZ+QnXLwprK1Sw`73?Wtn;y+4+c8ma3d@dB zl~z8%S6|2GJ7A9&u@fC|aB>e?+bv6}Di!SGOQ)C>ByY|{wP?oMw_9|)?9Tc2wPror z`!wVk@1VeCGd>aP5W&YcaE#IahvS&?9BA-&hXD9ut6(#^ex^qbePzgM(AXigdNd2y z&!39*>B}(qh14SEQ=tgYnsYkkl?IFR=7>2jbfjg5#!RB~gTH>H;6+><`-Ip}IYnr3 zizpFg>1kx<67lbb~XH| z4)=s&w^;RBkv-y(jpMkjGIE9`bx7bLW%vEr2A?pXc@l1DvSB*P%4vSZc1#z7k#1&4 zeG3Z5F%^S&7TV&F3Ys?dRy0dU<73gVtT`ED0muY!3N9z@L8rH_FdKH-`ws=ay8_YP#f-g$u!Utn=w_ocght8YD$HPLY*$h+ya^TGmy=c*J=JLxUJc)O z`E=5L6J=SsKoJ-#W3e{59_NDf`vU>!x-fVWUH94iaEV*%9*g@WD6SEgbrclY)cp6T zF<4(P>yQm-8jNa!_Exs!3~eoSRyQQCMeODvchQVA9keIeaoUo#{GP^czG$6thOh3x zY2$Esi$YD>*0KR@t=Ixy-2s+e!^#vfL;nZG8pBzCytC@I=Ug45W!koey`L|gv00L3 zzvS70`EMT!s?MfpB&Oh^Dm@HeXDh>pT1MghtoN{8<`%Jsr2b+=LHRX4z{~Q3rCO%X|iw1&Ge$?B74qjD>ta znY1_{E*Ty{Rg4k6JdT?zX|Ir22tj6P;H}*6@~}Au7Z9~R4$yKXK+3RMO6ao z??M*=QC_5L@t43w(B$M!roeV-dIe=|qFJ(IjQroi-)7Ko`aK!{{FrXexli!caWOjJ+mI9 zlp6ODORuEd9TI*UI#0>JXcqK4O>@7%7b<5)V~PI(Rt*$c#9aV?S)n;uSccEWDTi4i zxkv${n{k6F|Fv{pJoBbG;jWw6<^*xSV_1>O*w!UO#g(YD!)LfIM7uf=?$y18Fo{)K z0rHzK=0{3fVeMH9wt6)W%!19}48SOBf7X|6d}B{fL}rX9j;GXnGb28S&{{dnP)+`UQ8q9&t7AxTh^1A4f_M*Z9lnUfDp4Z*T z1}y$bngX>ReysCv!MTO^Jh*PM_A%gyb^k_F+qW>8JF0I?>gSG=6?U1)J{yp^+VS$Uw!rz zug{5>fEXK470?W*87P8(Cndj#i?;Rg+_grSDIf%S5?3Rq3f zU^TnCRIrha`19EE$w}kqiHcA8w{9)`aXUh_Wft2kunB;OU276Sn^NB+dvq^J~e=9bw60k5XHp?@3$!g{`2>-V%-tuNZKIiaK}dVgYc zb+%?aR;ySLxpejvy8MYQOIJ6pjX|cUrUuy*598Hm;o5!IN;15s~3}X%y$uRPddWIO&6z!MWQ?+`pZ-86b z9A^{dkHWS!MlM{VG1wLtZjzx0a1M>GiQ&F~M&20BZ@3yz7I)R#VQvYAPC#OW%Yk~i zvocZ|2lhDu3mgN&8dh|a^?tlq4W(0rkX{gVP|y%KjK#whqAMs7#N-JN`E*5LvoReK zFVk{P`#q&~>8|lsm`kCQDVYVidCZk$))3Q}5o(|9HLTEV0(!(voU89ctyG&m1y~0U zgD!OrPJY6>eK9MpPg6HM`LRV;_2SyT(iyw{O@5v=)H7mQet^EucfY`hFkqbV(rkI` z?-N1fwuUW{(phrS+sv-1aEg5JR2)&y`pbi+0>O*9jNDmWPw1efbCCMFKtxql|Z)getIlL;t``lQsn&WXOb6ZZ+` z4ly7N8QjuuxX|)DO;N>Ct7&ToFy7mu1UX+M_ThD5I#U9$+}l&k)*;S!ClJK|GKH9l zNho=F>lyclIr5V5HVD2nxa67&Dv`uj8AXzliE`y`GNXkbH6<d%A<4?zI~mlIP|HcPUx^8}QSS+Wn@ zkUp=ni z^y$*}y+EleSV3~d!@B*KPee7?p}_7b;AFJr@Z>sgH@DdI{=MgPyrh4@CA@O z5)ya|O28two?5w@jf^nk?p7Izi35p;Q*25y@*kuWOG#bMs%m(gF5xa0zgz@U^ma#4 z%-T-KTwJ_%j`&0T&uEM|0c;F?jEi%8UiVA~5Jbn3IR(rhOB7i>#ye{8zoUc{u?m$B zjp!91e7r^E1W^6vi-3Jb{?ROaK9k*H&=OUcQI0Kc=tnYzrQYtWf>QL7-bzUUR-~D7 zm=D!)R;5G&VFytUlUkJ(sRAuA<)3t&n@VaAz_KgbXO!r~a+ID?&t zjU#_fyc5J&b)B7v|0I8bv-+6lol1T_rfl~5>nc_b)*uZ8r!dLh<>{);7vJtyTgk>P z8iZthw6wHG4)WX;-eGCdGZx>r=gck?WFwTM7vh`W_&mFA_4je%R{aeNL=FvpAHCy^ z$kV21=?xkN+v=|-UWU`Q!dclu10f043wWdxA8-HLws8wS94Sf%4mdpS6YjlL-@k~` z>F^C}EI$YV7;`J!xE^17e(C*c%b>*NZ>jFT3K3o+HRbYLPh_Eai zU*KO@3oX`-K(t6Vid?On+8AZTuctcla&1pwIgLVm7LBz*rASe-{!eAXC?MPpw{3DB zq5cTo@Hfypmtw~O7#+N(lKVS~k^Ta>nW)vcAS&vr=I$KZO@$9GmaO*a%||VseQ;N; zZ{Osjc){ThP*X#Z{KdQo_ka6z6Q}OF&Rghy@n#HOGK9)S?=oU?WauU7j4tNnwK~5Wh_7~ zwszmG$z~=&vb*VP$OCB2S>x|I2Hc;&EnW-Xj%WT{RAFrz>4&nLLL&Zw#sP60kdnG0{gmk{UV z=-x`&k6R1i#d)D9^Pnma6sHw|wEYh`6EVl|rk#~G`A08yc=Gt_bAu6T9tkk2g278X zOr`pH6mGMKU`y_Gg!^#th|L!}k|y)77Cv{2lY4aWr2{s*((`*qvRB)6N98ktDV^Oe z7tWcl#+rd$rzsw2yq6Xpd9|=Te3RS)2t7R8(v z-2^&}RHQw{iCsm9m2^SSL${ujIH%f6a7eK$*^)#w)?xb?pAOsytUt1w2mx}BeCW`Y zImHkNd`0M9KN{>tp+XvC}yCi8ggjHHc;h5!5!dpC@R~bvw)~FTegHVfn~qt z`txU*qZaznQIGUwZBu1S$cy*wcwa6XVb&%(l1ErSGX|D+3n7+z zV;Fj=G!b=bkj(+fsqxR+`U+FS4e$u1p@Qier}^JBurm;gOW@6|tSOplCO`4}51fr@ z$Ao1*IDE;>E*}$T^a=@a=Zk@A&G7ZBOF$!y6w(8p4z=U}CoF|r;Ii<4r*<3YHG1(s4sTEY_rHHaalamof+bLzC0X14^xyyPLNrYz;j=K zb*(SsldJjtWi$(NDmMFa(#Ko#h68=QzwJy}cq#3_hR(!wO#^ev^t-L+9&50Ex^bGp z$9<{AHIHSZq9iX$-_l-u+a01)=Dp=B?VB2k=99L-cKDGQz)y=xfsiv2;suZ=`O@~# z!QlYI`K~t(uq1-=qmTXABQ)?bs7&4B|Ht0$#FW`zd%Rk%7gKFTZeqCFjlSTGYuf)i-yu3lTifJ<(d)5W#$$Zw50x7|u^+y_l0fXN)uB17v@FP*24c7wKo!u@9>ARc zsbLSCvD%E{M5Qe9ad8L6O_!As#GyJp> zFc0AM`A)#oxu_VPcS1*e4}5fO%#M$YFHUZzAA`_)Cn~o>LYy)Jj5J@ii;2I4+mfJn zq2zhUO~m$X-@*_hs$V{HjhDdnA1_zJ8#_?ev* zdZBs>vICE}Z@Pbv?oPI~v50pygcudYSRd$F6^wIB7)Y zOjq^l{V@|D4_s_~^=y>!JXs}}q!y-(u#b&TOkXagwr%m}MROdQduJxkJMQ29wX(0n z;C|V@>}Pw!wkcimYU)V#?60=n<5Ia3D1}PP;7^exW?*m*`UZ?hcsk2CMB@K~&Dhx@ z6jded2SG=UB-fpV-H>5aoe`Hyo12AL%bYoEIgM*TV0OlSxUb$T8oDCTWUbME1JQ8a z#ZmjLIbfMb<<3dkm#^$f$sc#>VoG1l<-71VbFqYi`Q+s>~gn2P}lQ*t#6;j?VWWEYlpwjO!%m% z6h73kG`7EXhUfQ(0n3j5P?Cf6z{Do&|L5eGWkPab5r8WY%1AZw7KRsdaexF8Bix41 zxYkADSAcVkjC#wPgl&}WYC0|UNDe8mRYuF@B>Hsz4^v+PS998iZE1+f(t;2zj!ZII zrIJDiom56fi1w7UsZeRN>!3Z-Zcs5K=`AIdNQo9i%d}Ax)4ph5zU%%U^L^j%_r8Xf zbN=Uf?&Z3#d*jir_?6fLlDOMEenDha@bgFAPY4U*e}F;=(FlL-#=RmqlH^0yE`eR4 zT;l2uk~wn&eVy`L>j8tWxfRyil_Kx{Y`QH~0J^Vfd>Oqcx9F;D#8AHbWL(8O&F4MB z@t?}FjrL^?n$hglqx(Z0O1^$xx$UFB#i2j1l1;PuGu?H=e)Is*-7tR;yuEI?Cr;Hp zaCee{3%d87)axW22!;jM5|)^v*5|xsRrkc`wwlO7GcH7^KyiG7-bAbo zx!LSD=55L*qeL-Nk(3#rg%GJVJ+eT72B-etzQ8I>(Yh=5>} zxNQb=p35OFZU;Ma4`U#&F}{e0LeG(w)h!#Yrkl}65JJC1w?&Gv-rZIXXWSRR6O z`ulIpjNP?VblV#NP*x}YKJua@Gxp!V3rjNhU^&XrEnBtYy(~LR@1veNtN6DEHEfR3 zgTuKpmH$gj@&Bet+IFg*SOox8r}xM2s%|aHzyA9AO$yCG{bB?rja~)-^&c=62NM2< zT||Hc(&6UpzjfyUc06i-ci<1oJj=}f8vEGq`6>ggXCb#_F;AL*9#)mih%~?{B5pfry*}m^4>{K z2bUy{+oLNpr4y8(7a=2u(+8JesEdpEJOlB?S?@Kk=9GYfnm>(^1B8~)O=G(6qwYXF z*S~hw!5_keK|^0dh){2YMSl9)70S3883+^oIHjt$!&3qoKc@UKQySL$tY&B0$#QIB z8(cTh^L)_5-RkVU0|&f&+s}#}Sl<2VR;#M_7yhn^iUU_JMBFJ_w>1R8#{K*K#WEJn zY^kXs)orO+sdGUp^C4K9E|PF)42ud%s^1*l2Y&h`{0FeesDjdkJuge|fT+BY5~DUQ z5frmwds$>hkLW#nvQIhg4J*%|Ie%+qPJ@ub!rL*2&x_vO7BP&>=JVI}gAY%R`KqUC z>&_{vGI)Dn->0#**2QyVS3`NhUtv{{4Km1Zij@DSxTU^w{RMChpl=fZHq%7puNd8( z{oBU#cXat)=T8X`3`JAsZ&C`|rp@2N;`^h+<(OyW$MmqeLGDfzr@Wro1yZE(6M&eQ ziRtH_a;*Z}c#lh%*FYo;ffE=&iYx;C<#(_Vsx#S$ufX|GS_&GOww)sg`w`$lf$>8_ zhv>FZXh5p`*@)CTFk9(O{rQdjUdZD;w|rbXKKZ1+)6Y3d`Y4vHfq??lEvB#(`jok0 z)WzNDYPQ>a_f{?)XL5T{=H*UPQ#BiHAxos{j$P0LSkZ^$n^0L}57#$75jesx`$#+YhKZvULZ z=HFcnzVNB7ROH4;gz0eO6_nO27EHJ*FC@N$8_!MMuDecVu_#MoYDmyZiJLdPOKucC z6KI}Nn78{6W8IySe;W>*T=OIR{4ZZivTQv*EIW_U_)5sg7s zll(-$YSGE{hpyfHoBh>l#;jWlhWCAG6$#y(vW8Q9*zV6n%P+Quik9bo`F++V|M~vs z_(PUjvXCbUx#_Pl_QmZ_@hjDt9#j42OB77w|MncKYG%t0HWc-m$o8MSd@xV5B{qIS z*x6)EL2{u+f3>Zu+du`<4sw6%mPL{P)Wsq>Md%TCvV!cM4E!W68ifSNHA2N-*AH){ z@GyD4pwoHiH*@764`hgU2BC~0T9nWyNttXJ`O#+Pjark~#UVbgiYRUYhX8hokub*2 z;`S$Q5j@4+TD+z}gfYAXonP0h`7=L5pa$Vcs2f%kz>dfWevViv1=%51OLz-5P$60$ z{t!9S7bu!j*Q)op=oYV5^y$F@qpqBwt6rU7|IyyNIL9w?M|9WGuf2zG|Gm~* za`yDJoUh`adPypD*DEeKo%LVX_D9D=K3=U<~o5ClRdTRdFn}1x} zTcvt@WcQubRCpm~{HZ=5-u0{Q%htF_IZRXJQ1duE&cA3Dwsx`O{@OUYR@_&<>1~t6 zldagAakMr!UizcEOlzDkP2v0izrxL5H@=?0xKW>ZZ_ZA=e0j$)^$=7|tetEN?rZd!6U{R4 z`*+PwjbYR+XJekNKR<>4h$s=JOzwh#<{@B^euP?~sU0Vn${`?8vvBg*jBg;ULr$2k zdUz{CEIN^n0HyJ7Ha?}a;?YH?$8F>$%J9FRaVbm;i!m{H3HWUgPHAxOt00tV3^6@G z$O5=78Q~TxpX`>nmO(^dngad1<5$8PAT~Ysj8bG>V^x!dZa}}72|fDMcS6H?^4QsI zBO@S2{v7R%WJbO#*>d!o{`rV;-$8dY$4;qjJQt9XI$Ux5y|rH1Vpr)_b`;CifNkcIb8TUv#_u-_D}Q&`x)Fg)h;6vCq!?Um^kwGONF{R%mXXfUi26wYV$A-;B@81{7HUZ1;IriOF)wdBd zA)Cc_1zgZ8T!%|CC^6or{(3ygpr=APvEj)U*`wO01kMR8-uErJ_f$ojPu;iAxG7{+TytEyu=nWF$+JC`3urLHEIc#V z0RND$*;T6l7aoeB{HQ(O#O0typ9`V$R8V&&fuh^UJN`32aI zP)U*xg-()uQHeko48ulNjsOoV6T$-qt{1Dp03p9p?$hZ;toqoSgdGk}xo-~lYuLn*xxX*>p~3k z<>``q|DL>@@Q8I`o#h%-l8U^JS=|R7F{oedWV`6!ePdTP+_e*}T8Ao&$?s_zW3Q|* z8E73zUy`O^rzn{) zOIYdVK&FxNt~LF?T|P6pz0BJD;giDG|K=R5AMx+0To^3LXMB2!wc>oM%{68=mck>l zMch=0g&1AAg@4njDSf|g9GTQ|K3Zi}=!6xtiOk z$eFqM{U-15Fg1lUG5UUIr>GStU7z6JEU!6hzLnx~QQ_%Rr`2|d2HM^{A-fRk=eFHE zeCg(;1j~;P_IRbV=3WT;vaaE-jBf9amci_P){A$7lb5FG&Nfqq6hbh{eCA>Z2200!_1)};jw(b5kBhkMlBJWFJp0$U~P3v zV;GAzq{JkKZ;|19rxIn#D8GqX*6ePtp`=!=b}T_bFsJ20(8fWrD&LMO zRdw|ybgW^B`J?R7h1uE8E`D9>6oqS#nOJDXR}3y~*BHPM*o_BlGj{O_@t^j%I47t> zW1G#HX$}rntM<#L8wG!RviQZjGsdz3Q#hip^kTSD-$vu?#Ya_lns{FDzrp2qco?7{ znflIo+RX<>Y?<%}^Am32U`~sia*2xK*tkwktaX0cBaQxY<@nJt=d_%h07q7jDSF55 ziRWApdD{E>jYhTBr}iwzmOeE>YM-~&9J7df(kt-&cHq@xhD8hReY|9-e)E{vykBFs zHAz}5omyzEZ_}rSo3n-Wy;l{tRY-6%HYF=xy3ttoJkoj@_cuI+pw4Ej#6Fg`STeK! z%gRky$78pJwM8B&} zti0s7q*gifutVQw48<2ehkxNJmU6-ke@ifr#*sMs?hif8>iI2nXb%=&FX1SMUe~#a zHEK&BCTNGJ4_37Cg<8n#NJiDAvP0;C1o&ZrxfD7#Jv8 z6jH*H!nEugU0zaW3}+{ZNS%?E3MT2%!_w{J8N-7T_?V#&@q&f8_fmEG^$afmlLxmd zb!}WjAwhara#^5w1`9?$agr=sJfyXC;3 zn#RGS-Wq=t>6Qu#2NuC(QtQiCMdou4IH zuz@w=87MI4O|IVpSs?;J!5r*F@|H zjM!25@zr(Cl-0`;L`JYcF7p_7M}!=0Dy!*m%qWZbtIG>h6=V`1BRIZdy-KVp#_Bs# zSe8pD)N{XWbfHQBtMaPhwctS1j>;caeLWI0A4g|HEd>PShw(u1sXS<4k4Hi>ec z)1!bn?@xOQmp;W}6R~B>6arW1v;WZj3u<(}#-Y$b1v07>Uru$iK8+Q}@u_Um7MoP< z!cUxGj0LQSL*G3XcY6pMdT<}27*%qc3u+hlz!)Fp(0#T`IU1K7S*Zp>TJvgQrUF>H zGN^|m5y7Sx{36#yELA`Gs*4ybp^fMa-f;@Nc-ABT*4ZgE^1jiIYj0M*`5OAwm9uR7 zk!0&78|WrlOj=B~G%X{UK4XaO*E5z@h}N1ntlR}tsk2%A?z*W8jn$u0-GX|uui~}Vb(8#`5Rc$(^#kZ z{7ZBzS&r|qlxpnCCJD6s;`p|FpKVg55brD>Uw*y(B#tC)IHtMT3J(qog(?*qAI=+S z3c;&79?o1Q^aHCp9Qu9>JuG!5%)u>`We^G@DqQ2A6?bI+Cm$I3S4|!6IF69|8F!?;? zNusrqza2mR!8n|ZRuaGU+N^}X`mSBl&X47a(_I%0P zn=t$ea_oO2dfF z;hCi~Jhkv~IhIn~LFQMlTTA~`Ugi=X87J>cHlj~yXund)4kk7HxoTjwv=S*riM81F zg_nOB9UZO9^NWA+LYumVQ51;Bk>nmH2eJEFc+Aaakw|Vw$VD6v*-KJXatW$H7T7D^ ziass{jJpNhmX?*p0d!#VkrGQveR~qfGxl_&cwPzm3S?I1JedgSvUMyA6X|PXwh3hn zZ-opcL8%!F1+TD8$fPPHlDTlQ{+Mmff}+D0k+v(Oma>Ib!MqVh8OON^bH)@M!Xjg= z@$1llX&m4Tn_g2g3tcZ&u&JDx(r9f096}b*V7Kn)9GxcR3{Me2m5_qbkOG`Fi>%?z z)KV{On`XJdLt^_4%;$9k?WZ9y$9X4Bwv~PW-tjP5K-^Hcp+9^TY~=J>*lDAC3(}1o zcH^{*^^PQ0+lXy1bh%xrOD2d!4Z`FK4g36Aa=mlI?aJ5GnnJZ9Si^NU9k+EIMl2(2 z_pul!zH2b34t<#DMgQNe%d_v1r8YWE7==>zd2OrnnLmp?6Ks&Tl$EWpEuVm_e3qf)q2`XmOS_%yf%QV)ETS8(4PI(K|@1_L|{ zU}@g*zd`PPn-H#O1=m;>zWu9fvAlqWNB1chP8(X{>8;sH>TV+>sGM#j4V5-u3Qfez zg&EF<6Ip_1JWOu^pKTQl2}n1BFfJ|%p>KUKhHp`BR()SB4PC%(!#eG@KZJTezGhX+ zXv5j?1h6C3iCli%3$6qI!Y^_8b!7-}K-0Wh{$I2RLl6q|nYgTPcp$b0jHV6=#56@az1956vL=G`@NCE<10Z(@r&-r~c z&RF(^ynNsa#zB@8sF0G8wSa--orY>)Nvj*o0~Xph<9_fD%^vQ~KXfg`w!qI@v6 zgBiYmmlP^P_92d4I^76bfbRzeUN3L0Zb38emv7(vX%;;?zhQL^XTy9wXd_#Jk4F~1 z!dki%o)rfQDTgeQcZYFHEuD!ocq)$Vb-P&aun-hhkPb#rkgzwb8^n6THtO~HBBF7)udr%nY;ir3qT%9M8-}r%>NUM&I(TXT7tQ77vHczIhbDTE3OylR?X9 zAOnaLTt0n90LodA=!g1vtPjT2p@of6?Ok1cl?R+%20Kk)6#(Zx7z4T0>NF*-PWq<$ zvQ=|XEe}g%ilBQWEv0}!a7q{y9sQsUYdZ>5^vUN!lHn_`$CRWTXM*>#f%5Wx6Hn9%Rq(9zdN4WgTy_o>h`WmQqFluAxKIYTFjgu|^_qn!Oa)YhC~|PL1d4HS zWUg`Q6mF&ITSa3DpQ%^Jy3PilFf>|Eo^zU8tUXJRPfJ)3b8TN5X|+zSkl;610d#gw zewP1sx;tTx&NKmjjzsWz1=;|(WW&^VB=xwa!z=}b8D|}@)8b7yGif}muE5MNf zTSENj3bI)b?SQ{woZb1)J=BMt6%n+S4lAJ@DOBAAQ-De~e||C;@0dH&m4U3miy+vb z!{x#J)KU&&#ub1P@L#w^d^&PmkOX=PAXI#0600N0SDJP3so}`DcnB6;6=WDH2Lovt zElo0=l0RZ8iIU0YeZ7sUBY-)=n6GVit!|*yU3?NWqg5#`@Fq;}T(Eb`@e2e|>gcj#tFw zkIA9AGY?u`IVJGqlkGJ1s84=lr8|4i?=;)mvpGU{ z#yB=*!Of%?I70;p93n+>Ar5I}%vmjgdCeI1sk7wwu1Vkn&N>QcT#&eKLRolQA&zw1v0e#X+`;(1$ z7LN%wZXs|WxQc@WM!`1-QR3gEd5BB|H_M!V&_-U9?u*5oUTyUR)7N%*Tafo zK;pP$>`!K7I{-%q9(nG(d1~tFv}1s*BgIe6stl-v70OT}JBX_*Dk`ENwXa2gpm$p9 zT8*vj?NYwm*xiAf4tg{xqLgeK6K^0EC*@-;CxT@K_c)?9m`Z7=A6uSowd6Lwd~ORJ zFFgafTjmFWYsl=vH-c~3#&5+qFHDTPu!+Afn2}cIvalh%oo{-RiIC(j`>)hghAmV> zW@P;Fe6T%qKX^SW@HLejDHu~hoKb=ZK?+6!Vb9Jnh7lHpyaO-fFb!H(!h8O|<6M5q$o^C^G&a=$SbI9*Z~ zPE(Q*9HyYdJPKcV#|v5k2F4Qml-D;teUszm{3tuS$TjOWRSuw&Ee3l%c`lU3sQWY` z#^mxBy0Bo7q|UgAQ2+_0hoA~VU($`}JHI9H;!+3(4xbTQVPMG8@*TKOI7_(G1QM13W}B1IF@w%KxQ{xZ%~zI^SQ9EtgS zH*QqE-XS-B3cCtpThVA^8}D-s(T7`G00SIV_{8)^wXu3oTXiIQlGL>3R1b1VqIp?yN-Ytk)v3exCM1S{!A0L%#@x-vLkwmJAf667dc&|#u{Zs|1bCG9f^ zWdft5)Y2pAQn25KBttyEz~Y$;*bjzzVr~KPy}W>!nAp*yM~M=G1IL5`1%c@%RW25` z7mMUfty{-DvAaNr{g8=|0M>w2ws&jqFnHI)pRv7G_C|9OzEY^dPzQFVdB-pB_3igd zQ?0fYosm13vnbQEZDCk$7mff#H$o=bl|hp!Fj$gydlrfN#XtsK(8^2v9~0-^M6f3T zNC%4pEOn#u5@UBwst{2L=Cc_rPA)!%g`h(7H^$9)XaVTy$^aF^>4B9g1}}OTDwo>@ zc?>rcy>Oa92q`{8u{r`s9v;cv4!owXguwmj3c=G7eEjkfQEX)uoC!4;=T44y_1)d+ za|shZv~0oa6kDc#>YAMB8Xl?2E2DLPG*BLOctP-R=bp7R2D8=e{@pi>7DC&Jk(5b5 z7K9+QL7F9X7*|I-Scd-Ho2OfK9Dr=(?L~k7m_*O!fb~OI6?O<~!wwg^SQANQHulo2 zn})p>sz_QqQ`x?KJ5%B>KG`E)gGJS1+o4A2!)cB8i_^k#hR;1k;;SkIUYwyJ_s!xH zpO>y#I1{J?l0j{>eq%*`C4OT;d6%W;PkXV`w^uk9tv5%<3+KF|4NTO#M)ONHLw4Y9 zX^h?weLgTUI5ZoOqu8&KJM>bnx2D|p8Uhp!PX2>2S$^SKF>&#*$bO%l@DoI2zoTe` zTnoTHNW@YbilJ`l+-y^)a0bMAG)$!>Vp#=Zt9gt{0*Fh-1CWsN%pUza5nl$~lDh99 zfD87T4tCZzuhQ~7o9*895QrO4#>>kKa{E@`#U^?}-$A!{i@}CvPdv*j%DvBF7y2cT zA_(m5?SJJUb_w-3{8C1JdH#TTcJFSy6`qIQAJ6LE=yA5nDYqo^;G_D%ev_hs6Xo&( z_)Lw1kORC=5{#t}KsX`NrvAVy6?Zl!8Pa5++8b=QC5Brsj;RIfS>m*ii zE6@_)k**A!DWP*7qr&h_T}3K_`J*BDF~s@ohY#A+3C#IZ#2(-XBQS5d2IQWI7Q|gl z$cE#2v~LYjD#Q@N#2_YtZf)BBE@>p3sqIs8)^sF=qy#V zJCY1$X71d%&?&eQ#V~hBY@0eh^1ihZ%8|vGfe6alLKOd^5Hsq3oM_lgYSL$#yIjQf zKYskU2sRWk-B|17cMNuU?&;fqYwyrMRk?*QH<(vLBa4m`uYxF@21a;yy2an>@;YKf zz@3>J*<3gWE)wV;k8nhK<+NJm^-gu4Xh=n-*WlVV&@vDOd{{xju}bIhh3Q5FD}aC% zfhdh-_1WEUJQ%~PO+p$9PKdw)4{1U|)N5fzfoEZR5Pl(kLfjb!r@jqQ_Yw{`K{7JL zHXcWS5eCnN=YV;E6AFt|#(w}6KNu4ejQHfjX*^+PV_tc*b8c~4N?zGd@A25V(Z<<1 z$U+nfRe`pfw|H0=6d{y3IwN|039GVQ%bR=ag|_BEPmr6px1J3i-OgbiNfb%P% zLBiB?LDM)v_0n!clPc=nN)RD3O-hN&kFW=!1aZ#1kc`BY0%P*aL~pr8;bOftlfut9 zHte*4gF~2;iuevp1QV4!g!Ko1!#Dzhau8yNw1aCy3LI2ks6-YYNhceCz?RsiX`7XT zte)(X4_bM-N6bX5qNvvT&kH?k%-eNs-b3Xu@LG#TY{J;58<~!EO~%$fc1AF-Qayuv ze*tHo21|ztlf(8iQK5@;8EYlQKDd|%Z3qJ2>)gyj3%%cftzY4h^yez;WGBD+O~@h7 zB&+@O3LHw)xDW+t#iq!Q44L2Z9ItO5X2_MdXxiH=1*4tm|p8QxXesaVPPA-ze?*1(_CbEcxSJ7?C>MpBvW0%PZpSc5r-?9UjraXQA zBf`12{iEzbOx0c6+1}k-8vV5U`L8h_1~=+A4k{zTF}N|zLNwENT)isAEUC*2Z-y>& zY8fQU(t;DDMd~#U+mf#u&c@*&%fT?)r1l_~7^?U!aj(SbFeD5-7APDxR2qg4whTZ7 z(MGWf6GAiL7u48VTAv*p39cMrEodmXZV)1jESK_*3{gH{E1QpC>v4imVbZ6~U#c!# zsKF5mKEStPL!TGRR%dAi3+$Ztg(ZmE|k zDu_6GJBXLz>~tgW0-I=iyUh6tKr$WvZ6lq*FEjORrrhM%Y)S~yj*+%!Al&=9T%Tya zktDw+ZSj%$Rc0r-G%W|xsualie}p7~1{jmu1f$_}d)JaIQ~sABk-ILZfY9vC-A6P=ch&j1NT+TG>dx&gO^`GrV8g0}*lh1|#eVoVx;39(}$5LFEg zYd^nu8vxhF$??Y4meOc4)rAZPQVjbIQbU>Mv^0xPs*Z09V)=Iblk#qGBLro9+~61`@K7$;1`7UDOv%DB1kG;yuif3j5oW%TG(f={>qS0c7OWo6tRbu< z6m{XWh3Mf67N5>O@pie0!x^r80|xfun?>%T5i9!mPjuD8wL|cAEMt*Sd3)F^p8>Oi z^8X;ykXj0y>*pLa>^(P9OiTc0C@;|7+0oOUZvWI{{1Ck?L3z;`Q5a|V5FOd*<@=HobhVaOLG$R_6DrBg> z@JkGb3Zd|A1>2~7RH@W zCf8Q8Z}G%udoxH$RE$z+@M<`nGtaJ5ricBO{Bi1egq}z@p#+iY{qT z1lU+w&Op8J!>{+|ZUOp%FW7S`(jZCaCT0#Dfs{IEE_MRbCBnaUtci36r!Wx>E} zjl2t@VX_!7fe;_@O#4d86fj9<@?Qk>D(Ej@JjiT^8)ka;N&8_^!3%;{Kvue7U)!({ z`u_d5M1g}JL82)y5G{}xZ1ZgtYqP9y5Yvo|t|268K|To4`rL~|&qeoDMv6)F zka*E85lYqNVz7s~yaYAR`hP;HJb6KMM_}3p+?3&SM&C-d@(uo-{+^VxbB}#Omlp>1 z5p4is3%gzST11;&bsSj1%{lRUcH{;zF(-wOQryL*r4v@c)8wyhzFDl+_xxA6Zpm$h}$Pnm*e9J+!#&5U;0=!8v;CIBoljcwm z%M?t-0zMb_?w%^{(=QmL(aWD8^1v()r3y6sEnmW-&kevE*IC_S3Ns0D2Y(rXsuPG< zDXhIoRVo}1(h~F%Fe71OPJe5JJ&Rp_q~=)>Bj$$f@LDULYF&l@~h&bA=viW3I z@{lxHaRxnpFowwi{{#;yDsUK9_mU%aByUP)+I(?7aF7RyjH-dwvK+z;E!Rq2PlMvd z8jxqT1SxN5DK9F&2)bMI1`eRq*8v_uSx92jAN;dr`^Sa;IK?L*G3V8(Lk^PMELuAC zllpoRNJAa)52T3gSiAP*n@88zn>=tfZT;s*8>|IwMZ%cFLbSWXC5kd>=EDxV?}-Xf zJc|l8z{K_`)23UT-7M+=NjbCzL$@BDcO;sPCO)RySE-`@2_?7|U|@-0ht*&z6?!m_ zT}f0RGXg0zohAsI2;XS{xEtZmkE#|d42AKg1v6o*;#)c}JjPn_GYuW3!Fp5;i8-PG z4)g|)$;TOwBXJnrW(x$t9}n%Rws{c#94^ME(JFd!Hfb-u4e_D$QSwz_2S_y%++Q## zjb`kiV1x;DNS#Q`5RFrom6A7+rXUfD13!&0 z2X^Zj??Zx_015!qGK77HgL#j;hn~SJ!KyD{2xI0%m0}IB;0#XjELpVW1uCe;GM3#U za>IN!ByIZa%OVSZV+G47?%AdYLqg+Vio;gf!OX{xd`JRgpA^0YPGWVsk&54w;%Gz$ z>l?h!Uhc|y6XfU3b@m(FFgA{9I8E7IkdbQ-OLHKu09dg23UDX*O}Jod3)IKJo5`I2 z`x5qU8i!nQTwY)O2{X-X4jnq*f{;SFD|d7)aukWwgiAB6@cW3t_P~^=;TKW^4zb>S zFy3q_N8X7Kft*}lf$&b?uP!fU>Jd%t1psI*0m4x|`f18kN&u2fC;`6K1v$1r&>ejv zp(sa^nGk~N84>bQG;t2*iT(oWflL-1{0c7m4r}W!;=#jft~jak z@%ejUc#`Y)qKKh_o>f^s?yo6xMya@5OwME85Md9z^>DM+h?UCvl%7(*5$SvO_wZGs zO|jUo9M$V*^QR!*kr$mWcsm?b1U$P(o|E#D1kz9(#rOmQX2f1s>ayiU1+HJ_r{kr{ zX;>Vj*c36(RbU3-Ie8MF#xS>*XQ!t_104DqS4xqH;B}_V;(K-K^d~K)m79&sAbH(1_fk)>YI~#J|43>|diZ`8{mC(;xAawq&&^UYS)0k)PmX@K^ zof1?Sc9?N6kT%r$+*(an1Tu+g{a~AaR@%H$vs(IbHYiv(JNZw2@sk>~eV{O4DQ(vQ zgF&IvNbaiEk~BO5c1{SNavTU{7fiv2q?+JDm2@LRU2_TO9(dZt9Gs6n`)L2ST@&r) z6IU=;Jlk3E{{As=^MNHC+6L?6_A6|?U|6%Dt z_ED`2&-uWoI0nce&JZ(`OKl)bU>DU;i$sFVbEXhdp5UT39%z6zY5rF69OE`q@+Q>r z09unllf{F%g9N$r?jo#(>7!AxlZ{{BXq-G2fDQJi29XLh-3pwd)Zs-EW=ADc8N-c< zX*KO@M_*~2>gXV3=~Zf@ln5w0<1}jkS%hBVL~`qB9QE8AAka8>L_FGev86Uqo~7Dl9*z}DqbfxRx52kC!8G-PlH zt6+)^MK(-@2%Hk!CHY5sKvD>I_f+~JnUx@I^pbXyWZ<6|i4aaDFyja~OXX03!#x#OxWH4O)hqWp9hrWvYGs$78nQ%E-TU_iPn%15m`%Dj`H>HU#>cr>ut0FiV+` ztxB2XQ$8QNl$Cg!PkYwaInrT{*p6OMw0u7X=sk+S1|(uQ5k#tS?p&%e^$NiBQoj~# zsP-OyV@9J#tsfwHN-=P-{uIEm7Mk`<6QFcuh%d_9foL5pl5 zo9vbmi86OEBL189=vY30-CY23ilQ<~*yNB8W#~kpoZ!cr%xS zWX(hH-C~-N6@fQJj@p6=iRgF;dxFp2CWKkgP)ArUraA>uM)Ar|wi|zi4|QtHEhP)a zi~&|J z^7FnY?M|%7hCd>+v2o;r4{Q02%9EEzDdzA+bIw#~)!2CBm=dX6Z-aVGqsP=(CgVOkhWl>_8L|MRKz z&f$iHY2aV~yWbDSGE@UYUts!2(YAr385lq)5VWyJbd^Djxkcu)!^h5n;YhjFrVbIJ z5r+HB%xvA=FV=4mZhyU@Rn9d9$nM%1G?9iY9#W45uwWS4u7;uOB z8=J|zi-US|U6wdT_^av4K#SonXq>X(>EFT4L1-Y8x5J;cAmat^4ZWahVgZhFf(UfL z4c{at9NCe%7jY-&F~KIUOe=RH2tP{^1eJt@xslYtbddnstc9}>rdXo-4Y^uE9!x~o zLgz;D;vp1gqH4z$0tzW40cD|-6|WDTK}Zm8n8FPL&jc5kuoYMj5jkooKod~e7LNm> zfFa{aA#4REP-%>HzaX9hy#+T_G7pwl#dA!nDl8f4>zE>eHt~fio9d`>G z2gnzC5wlA$LBt^~-hb2kPAGI@`-;9P72p5279;1h(v4mp(~bD+j>JIK;6^RqN$oPM zT&Z#Q)tRhI`w~MchkT|rWC|4lEfDfH_0FU=brMJPIk-G|8TkF!YdAy%(8boz5%7Zm zbM_mprG0AHil-Q1h*}XGk`2O6xQZK?;f*MDGsGyNO}(BztL4rU(3C>`RDJ|o`;Yw#2I6L>-%E8wS% z1^vGi20aQBPkpguDgxtn`D^wi7OSCSfk|0oMDSK!tQDo=6J?o|QK%W~q4fcfW|qs3 zfA{Fh;I%0og1T>nkDspYHT?`{Uu}Z|Kc;p9lNJywQmP4&6RJn1OD7;%Z7e&vjNmXL zeg-mw41`S536|8L{*@`rrXoqk9n%nr51B4VvbA|JuEPHc47P4tNhD zOS<{?G&_G56b;0WuweTh`!_S0dxCfdKu=!}7RYHVm6V27dmBF^QjmC|_a!l49;8?AqX(pP3(kkM9{rRJKb zVAWCh8Q?hZ2-q3qFXT%Iz#g1IX$(pgwK9hHK$E#|ods1(KMW2GM zGVhp0rXHuME+Myz=GzUfSitHT>nq0=%GWq|Dz5`9tFuQuyU5Ht1k+vqE;yKc6$Yuk z{%f1rql=nw2TMcE__ES(q= zQMB*T{rod24{{631^4eaP5kg0a!G6rQXu%1J}MXkTY|%SegTIF0mJaXrN;gSk1*K`3g+K}-H2zvoYXu$8E~~x4Z^D5X?w`- zeEP?A$X6xk$uO}lvOCD(*re4^{r*)SYg+Yf@tBYZNb#G<%et0nc=fWn0LjN4oP#2- z%B*g}bYsA2c*LS0afoSy-=q(yG0Zw0Q)EgTpcCU1dRJQoVZ19cN@QMyR~dAdD3w3N z$X4jTGWb}=80vC!$g1I_5Q`2j%{V zdJSP2bp^NHj5sxgTP5eHRJGD~?VDGH*VEIF69*Z3?yI~>p$heR>0qhzj` z&`8$FAFEMf3>)r@awzyXT1K)8i7B1@*w_tqIkif$0|E;bsdJ#qYe1ovzI5EdF(6W}Q@$jUvW8fqbM$fE z4qYySHv9;J;fxGmcPh2Pv-pnW+M@gr)gLsE6aooM4Ym}Pv2n!8-%6{?_rOF;e>KjCXa@UTw;$c4h?k{X*bl{_lH#?g7zNGxzYW19G4 zY}KrjJ#Nb+qCYA9GHjYYKf#cj+Aj>k^*O@Cnu8NQ5nMD=GV zc0p2@rqmgr)wXp9Oczn3KPy!nRY7G$N-)(=`wkqy zu!Y7!a?`v+rj`VzN(_V}iW~+v!W1^6*1CONq#i~=FA0DtVXCon;F3+jNmc#KmJuKA z;}h_p?YdqkT6G?j*f{KzuG`tTbLcyGsV{m;(WO94V3xv)3rOxi>jc=BH=?4=fV)l45Js#&^t`CuCc6OKBPXily>dH)sx$!x_l-g=5fqSu`8a! zfofyyQ*sJ{?rQEK+{DCjo-hI7lPw`(0uliGM~4obU{<){pRQ%d`fHSs7xV6(%mqnj zn3*yyd_RgarlEuacY+|_*XAOK=4TEn*#~r8C>koM*_PhxCWA>c2yWs@LjIgh3w0XD zJKYiT!Gl7mVN4@v@U_hO&qon#V|N132YN(T&EfzZqmbY|oa@1=iEhCDtk7(^!#X^O&#TZ}{QUTmBMxrN_Lf_O$bp59mRd z;!#~0YiVufcbG3(%PGku$m2`N=kL1aKNOcJjEHR(8kA!PMV+u09mJE}WQu=#S_Pj?3C6EBFjxkC7Be;z?V9@gQC602 zbjIv53LcZM{$;RW+L2^7U%JuBE`c0(mFPJY$`;W%4I83MXVeNzeiI*Gp}gm0TRy!7Umw3&aMyqYnotwhI?m)SJI$C z(5I1+s2?W-SjP7K4Iu?>DRS2ncl#pOY#EKOIu5X0qFHFus8D`~pT@F)_8<7EILEbe za%c>p?O0ocv_bqFdR)6p*W$MXd30G&%@yy)1`TW;)i`86!SMLrP%D;?e_l_-Kk>(AA?EZTn_(5Q^s-)lRr zF(Q>PXu$i%wT2B4Gv<%K2Fu>kk()>7t9WIo6{-|r?8XJNv8F{DeiJb{KP3vax%iy_ z_a7*>zR<8CA|m4Hfv{SAvDICOosIAx0{X-H7pkEn|}mT|U_HrfPsD@fBa zt>$5kgQ!DVjf1L>YoXKQ=FGAO?gl$|5+Op}o-*gP*RNmSej@w#!RxA)+4UnRr&0}$ z6hP0w6cj!C-xw^z{CCnIHlMcroLYgN6ED=0T#QPj>yU z@^CUl;m1jUVcI9TzR{aGZtBv8)q#SuNtP7Da&Q-rS^62OVaCC+s9MWKyjILAx?EeR zPa_TW>`$NGPA|05X;R^gIffNDh7LIr6*251$!)ymIJc(7Im~eer_KgvIS6cBR$gA? zpn^JUhrY*$vJNsO{(XIJ4c>kFWNsi4uq60s!I9K5fPc_`*swZ+`ADFZ1o@3wHF&lz zd+^{vby}%Lc}ZVMP0eDjjGE{~eA<>^G|81K!BcB7!&KatZHyzGx( zQBO(+$GyhsaaO&Pb7oP2V8-LTg$W|Ck{Pw-qNMO=9Xl)BH_(mXT5K`yj)6Eon&MCmkH|_jO&K=fG>LL>)nJ}QeTQC%_kJlBevp!at1Geg=V!{t{}WNh z>=o-V8|fyy#V4jyu?D1ZBd4VAiX)2NGqNtTSUs3rky_*MEk{XTILDm@!;^l&aYk*O z%~B5Rv4cuof0_18_ahts8Z=0Tq5p%L4YTVLJ4iH}Y_&#f23o4(G<{ajrJH1L+BEfq zV1I*}^a--p`SQ3Kd@4!2)P7veHld3oqIE?oeHv~jP86QZ0TpmKndpq(6r@#K(10ar zHu_H$`86COBkOQtB=VK7VJD!%l)R;7PQ-B>Jb3V^rzgigpeVSYmDloHCAr!eo@+Q71uA)SP}-=GW}H0aBgNfVJEp?wnP01Mjpam7SlyyP9RqgB zF141a=4mxRH#$v^4@Fx!DCt;2YH`lg`;a^FBPFfg>F0U>JYw|TO28N}|Kjo++mB(h zjd&Jm$puJfOl&L;7$0*!I+h?z4I9of!)Dq!O~I@IsWZ84dUYx8-*P~1SL)8-=|8+Y z9Hr~1J-HOa*8^X^uwZ~6A3_U6GD()|wr5U@*nAhETaiC)dX8O`$3XjH)YosU6^m+( z^o)b2oKY(ldJcXi^)Wm{Zf*-!Q<(EU4E0*38!_Gk78#|)^w-Drb#>_a@Cl;fBrh%V zsVyvs4i;)Qx5lO%w$U!Rr~muz7Ekm5boh+2_;-9j9<>+P!%Rbn*w~`#?bDnj0RNFy0Q!*5WKyZtloScTd{JOe&C}}{|DK9NI z+t1;&*R*Jcs$#T|1_myrnWiuU4rB)$@TS08buU%U!rWH3q&oC%eN^hK4dc|QRIapW z(IVi-MIHbCdkN;u#MmAlt>DZ*@tB?eJqO?gL$i8isrovI+wZ zc!8AMk<}PTIfJRk!#UGQAbiUIzjm4$au{nH ztA!OwL*K<0fRf53^Rk{iv3v-1Yv0PZ84~{OSRi*fJkJ@@A6*%^f#j1kpos|}BCrjysx%#n&?pUf5}?15y}SVIWaH}Q_F|rI_jFpjiPNAb+$l7es&5~l z4H2Ei5qne`=V)yI^hf1^l0H5AfSk%DI9r@1B)`TcxPWX&D&Usm@*P6+QcM5xj9cvB zhJM)ic%}XZXs6tku2_q#gO<^iOW+P2@pO);vont_3N6rf=o1aiybN;yNYMH^JWM0b86NENbn;pn4zYzv6zaA-4i`p6Gh}5O;MVqT#AOIbck7H+d6a`!Fl4OpyetkgezCc@D$*PAwbr+00?Xm zJxW-PfqB@1Ko%fMIuJny?EEuGdvbaqx`JSq2I#0@lqCTsv{|W`kpd?O+=ILmWXsr| zj4IS)F3avc-iKdLbU;rP`n`qm2Pdt5+!z5EJC0U zUaiA@tr}hfSExJ$??)9J|IhCd)a9Xb%8OC`#z9k4rbU-0MvHhtzo-_t7ZD5Iui@jz z1+2JAjPGKN{5V=BW%&CNy9ZZ5M^(*Oa3lUjfGq zzncz}i9jX;KImjliwgN|vQdu$SaVVP6zb8I{8)A4#;>D|lbSTYglRHm&WIRA=s4pe z5F6oZ-*cF4s`snxjqL##9y0fT!7d7?4Z#WwDX>ZVyf()jd5Q8B6pvQ7tX(E7sJCj> zn0vkn!iEavj#FhDr33NvwR_Ecj#qiDLP*P?aT|nyJ57Bf<0r>j`^$C-#`>G@B#3Mq zy%88KqyU2PR#{)K;v500A{7cE$&+4y_$up@j*gAzO%B5GP(c)f-Dp%5V+4sQ$fF224tRFrT_a6ufv zq-dyz0O`UEJXF>jjdDuPzzIg!MP=@UX`%pukqYe`_ZCmUXA`^*owwD)QY%siXcOb& z&r%99TgTEl&h`Ra3Je{r8#!B86{^mHoe3^5A^fbJu^3}2skm`xoW^G&1`t&IydOx` zPoPTwp+S{^PoI*s#$_my!uoiD(i7{!v=4Vqw*MzgBPsd2t)3 zs~1^6JCU70;T}*@DNw2x-U#mEopG}!>f2xsn9%JaeU{Bak0EjZ7q;bbJ?@j8jbA_R zH9JJ?3Di3IDtQzdHO0i`{`^!@z~FMKheb09@F~|3*oRyyK95(1r@tfg<=)5Hwe2P* zCN93dWmaSyJ2Cl~AgBVwj~HTd=%Td(8N=}?j(=Dx0ky!9!2J9)Z3xGTV2>b4jW2GPEtSN-2j>NIIZ2 z_a0P~jYrm)njTxrsD_kM(p{EQ&LWf=Z4N1iDN_H}^}X%+{~Ye_?(V+#_j4WI*ZaCY zpU?G)diDH$1kv^^w!Lz7>%E<=SF_f+{GD4#VP1(F3TG$NqCZ1>b?ocqhp(9cO?4BY z&U`+&LF$1Yp0sM3gjVw3j5KxjLKh z4R|QQ1JNt&z%ssCp-6#{&q#^HhtAmu39TXF5!d*{C^!EcsfnWWmpSK*%j{A&4NM{h zqX5JE4jmdpxXEpG92hEH5hrSsnEkX0acbMt@ip5e{&RM|KqylY^5uV)uFVa87aN!L z1cXQ2XA&O5n18rts!dc(s(~Yz`Xa?94Uc!vPmfGW&@1}YXJvc2S@adV`GXZfNHOY1 zr`Y!BQ-D&CbWaI?B^+52qc4W9O{u`_VmD>6yH+ea=s+msDzaz)|r01l=J@F3nqq9%P&;00@Q$^QtIvL9>`C=w`!4ykYoOx3*~cs_Y@*-=AE&)ltI1My`wz!EYL5Pfum4(-7&NBChd2^jE^O6bjG&UhfyJE%dB= z9YUc86ryr~Lw@zYIz?z`C`nf}b|S)gW^#Ge)X66_9x7!`=kU$fQ+_OF=_?3FD!b-M zu-&fP3lHVrHShi&kq+M3;r4!E)SA<0C=y@U-hIugB!}+v$$yvJ8+U&-6*F=kA*s2z zIHL?y5+N*4=$cK{!tVxejQy*!y|-jUK)xbx>SdDgzpZG>PN1(-+FDaP9JB&v5DF9% z6&;hIiX+Y^Obssj|w082R}8I1vabXe-02_3P-q)8+oVEf|l@QUB; z05v*WihpgSct-t|<|&di3Cz5e?BBdc?rVTW3)>-DHy`a#3pR#_laAtGXi~fcGD78( z8woicDUk-cFwh1L;M(+cT>qh0{?wJioRnSX|8bm}*vW_n1eyK)u5%kS`6XP z(Yaaqd3pXM82|g_1*T8Aw?~Qr(x|ki6Z>rT&n0 zUB1B071?}+t4RI;HAU94$Ak*kX<1i)?p#;JnIB0t8(Apan^&l5^GH75g4;!Pym=|^ zC(2^*Rl#;2Q|VD`>L_CQZWJpXZ z9==A+_>q()3eGL8-|E!-^fIh~blZg1m3iEx;}8~0%FyL&!VEgj$~b2+u-K(N(EH| z4Y5S&10sDFwILT@V48r57?wMHjm;}HH(*5QX?XaCkpI<0$bTknLn&Im_w6-P98Jl{ zMy|p9{f|BbAeJ2Hlc2ZeNgD~*nTE>LXcw{1kY<#tCux%_W9U)b3-$6?e|cmt6QJ2=YJ+UcKZmo<Onq}BwK=Q(YOz>55(ae%h7j^NDRPxkCqtP*$LG>Oin1pOYxLZ0 zEqiYeh1r^gr*sVe_1pld+;`j<7#c(|8(&e1B?dwx$q0fEF$pU+a%Y0{FFD=s&BcBf zzX<#vdHFx$Riy0q=E3Z?mQzkr{G%vM%#|`WtWhGzR_*=_iVVU-&&~YlbcKF*rkJJ~ z;w{QNUw?=Hq@X$$X1!Aad2#oTjERw6LLyEFhaYNIMT{FaS?AQW?uG3)mvYR5!@~wM z3TX9|j$TUQF65}%^5$*QDX9U$&fkSYt~RK?pBd`Y^pG(3<{chrWQy!gr zc2-Cuvlvcoci(HJKOQrp+^5WqN|z>ZMO#eP8aPNY(O(kzQv3ng!p3nN!W1R0$}!u% zZAc+%0U}=yL?~1TNYq!Jb*z-BXVa2(h~`)6ua{hs2(wdauj}3!fAR8p_g@$A27uCXJfJ zWA9R@WcCQBKORY#I54)`k!uW%npw`dC#T^d5<@|Y{$vdj#@6{sAppbMLc$~Al_%!( zD8IFcP!PCllCZ#XyI3SNb))%{`yxNqa7b>d zDtXpGk@d-!zngbIHRx-&&`44<`jFF<>FJQ)p)hjzzy&!b1UGK>9_B@Cur9QRneA=F{Oe z97`7cSMTe){8)6sY6VtBK#&0NzzJX0LSd>yNEyZ`OB{>`**!K!;c>Nj?+z>c?@lJb zs-#pINFG92nFNit=9;oHU1b7`DORGJ)Mx{xAbVgeIlRm{NOOjomhy4zuV5(J`D^V| zI89Fcv;K1u!XS%bS|1j-FO`eJT)rM?4g~`%;zHIu(S}rJGa(5dF)dPV$8O01GFv58 z9w|Xo2{%~;N&pE z3}x`PKir(^s4Gz?k?5&~n*UG76>`qp?(ajKTHh5v2KM^LQ&jzWproq3Wif#`xKP`3 z&=--`A?YN&FzaQ~`STf{S zyQR3?T3IX-b!X!JLl0+|{C7;FMf~EradX#zni>vfk)V&8k&ayWBAo~-n8Nghs6d1= zI9^!L0QjP0asuu02$kINsZt@x&(8;#p3Z2maJuW)ekQhpLfT?VuObvlQO00cN*yL) zQZ8#NB!qEUEWOp2_$e+ymA3iyRBD$>(K&Bx2vv%a-^s+1vr#FzVlMR%fDk9p8v6YU z7z@+E(z}sf_x1Qtb1r*Dr^aKysexjRRqfMyKI|X+p6t~?QDJD-Y zxU;xz!Q3l5MWN51_gK^L?H5C&?9Akga4>;PMTLxK5{wG>?_7=5!G%6}TOBBuV!#Ug zNaB*nS-abhloR3!Q;}FI?fPV{P$sK>nvp6CF{H*hQJLVVL~qE190tQD*<8H`7ws%0 zee0n3z~Ea8ArjqkXakI@@ZjmXKwXcfvzbd}-an`e2my6e4 z_4a&k^VLV&zPr8m_5r8q!7j-Q1_rL3pl8r>;^US7nXzle*I(&4C#^J^;GCPC{@bt9 z+-(bfv)(qt^Z1ivZc7L2_J|u?a4)FR|Nb%To>r#ZTa2oN#%29t+id)1PtM++a%+f1 z5DQ-H9UM5?ta)c_>rPODehM^Wn`Z(+Ht0l${-v_dv1heG>6fen-NgpR^CY`khCF)v z_o7{2<+8-bYCeX^fE7FA(kiC0$fIJ~T(*}ue9^}vgKb<3S9<@$=H2s^sbfBWR=#UF z0gP4m=cu=@O+IrkuEYCYx4tj;e^^WzE-#PSip2wMQl?Rvacg56vJH)rBU{^^k2ybU z3Q)?`F27`P*Wm8Go;fUvnL$7+oVM^11sDwdHBA_y6F4$JaBGm_{M8of7-1bGK5Y4k z*MmODTq@0D*lr;+Ldq&bTg)(t4J7P4Jx%DLDiSEP!_c|V1l`Ga3GR&9nhStn~-O5wq? z^EoKWE%vz^TYg(BW6uZM)kCJ=Zl0y>;mZ$XtE`x|oC~mR=sr4Pvi0QCf5kQ%$Ck*N zJHjF#r7Q(M_>kp>)r_TL+QjyjK*A08%uRBH*k2SuMsKHE>{*Md;BA8~sJ+Y|%o2!0 zi-trum2`=Ibt7v|pJIC0`g5xJ(lQzofTO=nE4tKa$Ane4vfddN&DxkxrxGVwcpKh# z`S-hhm+3gkoZoRns-V)X}&mSCFA&OO` zxgB6-@s)Htr@iST<;{;C9!uiRk0lMZh3g_lKuRyAVP(@Jugv$%4r=rE)JCEkYAfDZIGa zH>Dy5+5GOQHNDZ>C2Jtp=Dlb|%0b*E0h;C1_{4Q4^tephfYIAuPuB5s12=CC8nZfu zA}-5pj-G1>BbCk@!EY5!k%+pQkx2BF7M>uYZLZ>-x zZo85CrZ;WEef+ZQKX{{7bo%$hTN#~YO9@*%v?T8{}oHg}#jIN0vp zRkU}cWWVC-=YObbI-0t)RrH|9C<^MV7;K7P%$C5he zAvTAm7#C&j^1_unsrTf=uoT-<+)ByPP%o#+u*_G{bj5u3?y<+26O<^!Nrorr+*t7B z=^qy@xp9s!2@Ng5ovBulFfkb-?PF;~Pv=M3wiUCD>!B}MnzNF{3&kC;i=WZPMEZl$(Q-BqA2S6$sXR$MS7Zpny!9Da+*)nYGcBI@$3a9cRdp3K;>y7s} z=NK!?!F_SqivI?N9g^wOLxgPTOxxskhx0B|<~#KZ>< z_9Gl|>?CJbStC%uJRpZt8Ni1z`6Kve5w+v&@8L*syRrswd&;BDdU!pd_y<9lsSAG{ zIpzi{+6=KAG&5Bz`IL$?BJ+h$hp9^TQ{1nJkr2!-5g>|>(w0cM3()x+t{W*o;LH`- za5(jSnMU^?ozhfSrvKeM6UGN{C=KmL8C0>YVpPgBV1rGtY<8q$s`f}w+JDO$I8vOb zmZkwRy^thRujF*3-gNeDW#L3C%;=1i{%PrL0mx}h2iw#J61JnVU#jRy#WccWj0Dih zI#^J_WE~rQS`u9PB=YFWA3Qy(iq&3GEj!wDoaR6P;mo1aygPhtfv)9Ob$nKa{-Qtd??$#ss96FXa za9qXU?GFoo94>7DD@IpweFq|tKCymK_Go(VMt`Q5 z(`we%$>bgENR|aF%MbnhWY0LRqG(gtp&TrSEWe*lGctPJpe9a4!O!r!Yp2NtV`}i` zZ#3tX4L7ugj-v5q3!6Xl^7HS~?p5qZIXy%cTTqcfN-#(zZL}Dp7|ddrC{O;jAHE$j z=0+rAgh`XD@YSamjjUJ?H8rNN1!xNyUTfWLuTxe zLqRzSO>0GR+z}n1*@TobjmUE`h*mb+H503E<=$S#a6kPQ-4SS$fL&3=gbWuJ&Sw!d zOX0Gv14#6-F1Q^)-*6JQsqcd@vbLDbjj7F?UL!dPCMCVN0Z9-=!2yZsUmOsUPpx9) z7#dGtK{7K1O~sf1p_hUa1zb$vRADr&KQ0&y^ri~7v=>000o;X8Hzn{ssI)k`pVoS@d`t+8k2FcEYq+F!tZ-(H4WNbWH3f0jZd!csV}g1)}2_?9(kqYRYJ zWhDB2r#u?IPflmR>!h%meNGPEsMPL`pr%_?BuhXD=ww^P9LD1C!3VU-msP!Qw=mLI zfavar3*d}wb(}yL<47xZZ1>)3e!Sw@X7{NnO)Y;`Llo?`n)f?|Pj{>Pa+xE!vPk%- zH;19pZZwotCb3h}R&Gjqef4Z%lHQ`hL@+S%*Fc$+!%_&QpZ-fBTlTvv9VV_j+b{OX zTA#V?{n}o=g~S^>{hIDKcc1u3edekJ6@mnrgwppwPDG?d21u)LHUj3~ZS;st&Gn~b z0iG}!w%@b8PL@>c__T^Ir0RXA+_E$1#QZC5cBTz>T=P6m+Ma4*hFc`LrKShoh=tI7 zjpI--Ln`dQvmw8Y{_kC5J6^_CLPI8|K3m@L*K)LuPAQ1gP9(a>DpXeH$=Oa50BJTv zD71z!bM|*@3A#lsw*&RM^ou^h)Bs)}%Ziwxak@*w!kwQ3)eqemWzgx!EViU`?$C-AA5Ny% zp)|DzdVw3o59sGdn&>h0uHBUXd`j_6PRYzZS457wYCI<#-H?jSeIh!g?}@FycVGAB zp&x7v8zSjrC3yw?wucDTe2)_g*c?m3ug9f~Nqz(mRjk>K5uu-@z~uq;jcE2fe)QRtH9XtM5VTyd+Ca$@=r zGg^>d!~>e|zPIi~7glz$z5^8=oHg9qudtT=AL*@-A{LdX*ocDV z%qswRL-(zCyD(I0!tKrdI=+D^6XqbfQhOrIhNMWiumz&ESqe$EWJ49xir&|fk_shP zI{UklrD6bZWe%s215qlJ7$)zgRT|%&R(^-8J@U5d1$h+yygCK@Qi8I)C^Pvol?!aj zOeCAP$H+=sIK0#&7BCCM=7o*_loo5;y%+^-uC#O#(m;ot6G27Hq_dpl>1OmAQS7A{ z!*{Y|k_I#EtFBro5nRq+C6(JMaE5eb0-t54=z&BrHRiA0`+6u2Ng7AHE}*ujQD&Dl;(*X!ZnY zw!-rK%8s@f!boMDLAFYpuo$Jq5SW0ZKdY4`1w4No;SQvIOuJd7buH6bl{D%d*!+nM zEr)s@B~>a>Aqwu&u|qj(Nd`znWsXQ@Y;Od^`D7NF5J3QzBPcj1Z3{gqo^cnI|M`>< zahHNK3nZy^aO?pYK8erwT1a4qNMUfS(^ja{+l;r^5@IoWdk2d$No61;`R@F<&FOtu zD9%rely%z?XcBLD4-2BHxtEoE64lvH|3yetj;oUGPIv~DN@ULxM*#MmLL62?voS>C zXdKl9a!H^~I)QCqT@!#9!E(u=tIhK&l#AuJ`&o&dze;B#CxlWfY)6%IGV?X=*@SYzlQJ{C#KUhD_sXH-FO*MT{Z6Uo-%Xyu^YBp@3;!o$k|Ld>1;?dS*xoLd-GLZelwQ^->FmU|;( zE1ce3b^-!#99lGgFx5?|M~SM4>pozFXbF7*!eVB zDbVugjM&qXWO43sY}n0$OLO=#I#E~lx&B={}fE30~M$__oo9v{!_G1RvQYFiI@LnY+SJ<(O0;tm^!8}L(k!oQK{|VFI;k6VIIJ`pYOmhcG zz$L{}G)@T{ddUSFq3BzB*iJ#q|HNhvzYgql3%0;Fm}{e%w&!NgP~ z_KUQK4{q+Pztd9bP?Bh_HuE@z1gmpO8 z@Sb^{rIg^OdvN`pEV2m>H98e2$9Nh^`wQ9T30)L?rS`qOLI=y^sPGy5a9|Glw8ar0 zGWj^y)ri)dJZM!Y$DBSQP@h(q2&GdZG)=kL+;@GclHk5A3Y5|KDsUHz7znmXUjf07 zlnNBf?#DX$52KMqYH}Y8fP*%ltmOiD4@6#XJYi))S8#d;gPb3RQW}rl-g8@8-&ehADt#IeA=-9NX}GJ6H(gVMG<)- z9gUbnlF4aODLJB)goQk18fSmsY>^RI(x_G%D)j&!*0kCjS&v3HuZfWL~3}z69hb zG6K#TQ}Dcbm>3CbUucZSXA(My8gM{4NmGtjTeeIXheAsL4`p~oPq?P2h?pP~D9RxQ zJcpf?j$)@Jp;$Jh5$(vKQ>Fp$ze{JC*1PY;{zffjm7Er2XSw*?s zgxuCAUu!tj_%#R@JG465H+OYULAz~L3Z%+;mi7G52W%IjE{w9$ zsbjaS36{zQbA{AUq%bIl_R7c6W4YGh)X}FiP$p%l}ox-GY zmb=jeWn&R|y{$@GbwUqGU$D)>sAkexrhT;5A}bw*$&c)`qSNP%7ecq5T{Za^Uj=3}QAw8- zB`Gaip!noM6RT-Xfw|7JHDw!*>@Swv{|M{GD1@!ZU6S%jS9B<44jUY$?<``3qFTn! zhG8z_7UKKDo*9?){}a-woK$=R5i1cI03}yBu%oX4sx*aSB{K<_FrQCoFeJ&@6x*7P z_LtIW5$1G}1KleA-5f=$433Y`Iu!;wb1E~=qo^7vFiOu#?L_RLF>k8~Bc7b72$Cb0GSq#E=wECndMf6z_6-C0NjXk!eqinfUCavsH|h6-LUiM{d8{a^^FH zMe0l^ID_@P{0gUbh!a9_|6G-z^|ozMy{jK-$;u;{GRZkdWRsZ)e|b1TERl%MI=Uxo zg)S&(eJbZx%7oqDSV9dz9Q5c_n$BL}iFl+E5ip1-$Av^8^R1*o*cL`6AWx3P5kL>e z!;ux=fv5=fu}47UC}DCgfsUPe(mS3DqIb*qomeePzw8v=#WN8dmOmFdhCxGJ6@#b? z7d8g}0Vngf0>whXAZ;-+*ZtQG&Z<*lJy#SMJUz-_{$On_RBFxwi%L0BNp6sbj6}}* z5R($aRBV0$?fs=pFHP2DYF2yn8H^n?AkY)II1mrIk?5+loW`2o-++&hOmSn?k1%f5 zz9wn;1aM1k_O{|kaAHvj-7Xnu6?}qLC8?=l>())*o?=#OIx6yys3Tpo@5%9jk^{@) zP{-&NLkOa9dAU!Fk(sSem);{h!bP+dP^@lLY9;0;$j9Q2s)c^@3iGpuy zVw}VwN`(v%4yB?h$1YmBON>WS+%ASLa1;dZ`bZ)ukCH&;WNBx?;+`O+av4%*R_n# zI`fu)Bgof+2Yssxw}cQEWgkw`TP`;eU?V~!O%#XM_f=ZvNwiZEc9E@uN~CUYXJ9c0 znRsU%Iy2E62Ls-)^ofs<1DHozN*8K{*YgFX%XUzu%e|DGL3!{VdZ4(wQi6-qd&_4e z3kQBBL68my%#+``bxZOnY6n8psL}%;^vzrNlUU!`-`V#`{jk~3?nGDr#tY3$j8AmD z({5rC%X%!%NN(#QQ^684{6R86jC^w!rXpQR4$MNjU@tDE6r2`N3s5S$fT}VU zrq;FJDtQuFj~2ZzR^6vp5W3n{HAEIPS#OK#t1xU?^8ic^}j!{#Gj1D&VUSROSA0;y|{h3SN+A!_u)3t_ONB1V&6wyO@tAIEdzi zBcZwfebI_Z;2Q3vO~GN27HX}lO6XALr2d|BBWZi<6_@B5g`pvMVJl$sk^>~PvTS7C zi~WAA&21U^u20z3q_vYZ_G5~Bs-FBKkc~vvtp&xeC*YFo%42a0vU)gP$tGBNMlE%S zw6Ksk!!9TYnVi@ro+qS%1CA91F)F{E1KFogO@t)r6ZLC@p0=Bwk}qHj8|Qt9S1I|u ztrEdl9|%-MNJUP`2*FM_qb!Sfr?-`wLV|&1e24GA=1to`D8yK!Q+;a|aYs5`5n~zc zk^_&VRl3aUOV6xjto_B~!Fpv|vqa}d+oh0Y3m6lr$UTFfjYiMRbQE?I2Ugl^(K(O8 z2BBYBM=FlQoghPggJs)YOHWMHGCFLOUDIqTQ_k6y{L|0Cr_=Q>^Cz8pGi=m9WbWv@ z%89SH6u+r1zQ+>XT3OA{+5%`D5+jFAF&5EcT%x)3W)NXSHaa*cuQ2CN2U7_E5%S6Ag;*S-_0oZhER{c^EO0yO!?-K z7#Zcd!wO!Hm`K|PN z^Av~4ny}i9-n>jje6o&qW{6B$D^#8^_r_d3)lF)>()*hN2<2ujPOzim*goU0+J)CI zJ#*~-EWCJaPD{eJp5sTj1((_Q5BX4?q>B31<9vUI^?jUapl%K6GoKpAWz-3c;HpOvZ{K#SPr*7tcV&vyybo+7j zwcF;F@dX1z5AgePS>^gZ?sWs>m)?H-iG1eQjS<@>N4oDVIR1WoV}x9|@csCu+~sGH z?p$)YTzAOr5&TU4vzDF9m~Yb@X^c3)khaUq+XrrAf!WuSji{ra$;#|`c|T-+ zzJui^#}=RebA`QqpKkYe-``!twA9+CzjIxOpX!_cpq1To=H_1Ad08ZTI=&&1uBpsC1ONtist@Ub~|Lt4+Y<^Ix3<-aJeC(yn#6(y9V;)`mt)0Ns$x& zT(*4b-h$hM5bfyl3%8XQv627%l5cLv_Q^(k-ctT=5)B^nHoDILn*Rdx8m4q3_uLet>Rg9Em)(BM znR-K}=Y|{#*;U`1fgT$pF7e@019?he?wL8b;*0{YG=?Ac8d+N{M6IId@C~x_h}E1bSgb_rK7p$4!0$a zS;IxcsdwMEGg$87nR&Wm_vILlW%+rSU)k=a=3NThVjjrXZ8jTiTn%4V9G zF(zHK8leOl%r4*2pGWLtP(<_1+>j#c>fBbl;OhG}3uB!`p*!Y-5FRkZTJNx_*T^M(s zs&8IdoogOw@6e}P{nT!oCb7k|z3!{XnwXb;=U3;3I-0*nUBkZZIep$~p6v>1%-`~j z4PQeyg;d`<9I`8Rfc+9L2Zw%|X?6vgo97g?$TqN0=8nOP{PqQI6wL92&#D zevfj@95%#rU*_)5cVq>e{}Xrn+|&G1&*}Gfo6jrFnkkwb*|aox?*3sz!lu7W30w0S zie!01k;6}!Pdz<%Y|nh^a@OITgXfOSr(af;?d;yCVRvdQ>llk=bDLB7nfm6aIiA&< z`mOV#1Dlt%Rmq1wvgPg0T7I0`XYShew$^^p^!e?W<9U3Oe(}q-#gp!9uTWVN+wo>b z(}SpenMvs8&jz;m#l*X&UknQV!=a%}=IPftHWY#zon~Y{aAHi)xcxQ{&rfQyi(l<8 zUim4;ck~`JxA%7|R``kMJ^f>v&kCEq{Oai1_@DAt?aTDpKP+teIJx~zpYK@i&Dc6V zPHLIr`KFWxHJ2NrHstC!8)}R+m-1wnq@Qdw7-?wGW?I=q<;ZEW<42yY$7^1XmkS_} zm7^Oz7tt*Xa@?P}RoeBno2J%Zx)h{2__%X!=CQVFI6zI>h{V*pzHPl?H$HjQ`Gc5O zS7NPj4@7%;_h+7~KbB{YjfzhWf6qRuV5jGcXATWHnP>TcrJnbGX`JpbC^r(Kz+ zot$?LZR^_JX`-8*D&=eHnFP_vI&V?pzN!|L6Skf8N}E z+0456Q0A)%g(nuq`%V~lq3S|hScpU2pmwL|e|PliS6iE!=J3-7!pVjP?uLlRfRD~Gi}Uqu4LaZ2d!2nl zTPK5YpYPZ%7P7YZ-m{hm z5y#UBj~2Cfx4kpcswCUsOtt}{5S5L#%F%K5ZFQJi`(EgY6DM@7@9*y7bJk(gpyb+< zJ7;>{-`)6`=fQIhzKUIY-kZ+{ij0Y72v||#^vu&oD@G{DU6uy4VEW+O8@+oYC0Bb$t}R>WKK zTj?qt=TTOja>Pg!Q+cHBpIrVd^VNuech#R?J)zqz@7u7>&gj$1*(yih?(lHWo*E-} z!va23o%gBdTt3KX`L|)^)^m4-tr?Mg*5NjTo$8YkX2-4RITws>SOv28y@?3dK35?A zh{K|_A?I&+-kZAiKxW}J5#H2-Pd!5xHyq3KS?r#Ee3oZou20&Ckn2J2fro7ER(X!K z>k}GZzENEK@a^p22;(X%v(9!p2xgL1;WJ+{c^~+E#0g~jasoj zQWd%Rw(iE|zwN#Z&@Rgy7WwnAaYUk5W7qvTqMLa`L)3$*o~yTr;{a8PYL?2)9QkX| zSuudwcF`MCXJ4I>J;%=W=B1#*lN-Axy4#wHEJhg`U`+gTA7v=EZk1!8u};p_{5r?Z zr%v}%PiM=xLCZFu+PtaE`|};cf0j@7n;E_K=1vqJG&}ROx2-zj;O#6`LBzqwHxG^o zS?qox$lbecS*KmTR-Hl?pV2kNO0x&(+3D=M7nk}&&JQCJYu|BRv3`h2vX|Pwe^li8 z)|$KTjk>@4pr-=QpNNH7d){06T)*wKcOS>|mos0r?!Fv&&LQS|#V!@Iv}<^d3k#ZH zmznEp>iRXkOtv@H;j?5c;;$tiii_YeJZE%P&V~hE0oRlJC(nx?q<6Erth8=f&Qfld?|3fknCmJoE=q=YjAi_5EtDw zsPTH-KAQ_cI%aB2+0rzmHF4jFU4tSzIqeQBn(dj6qkXUSwNr}M04+6#ahH|%0^6?1 zpcb|KW-KmRBo>o-S{Or2=4sITzx=B2;sXS(d6~zA6ki3I~$()&`<~6jOJSm zJHmOlz8avp5aAq{HzFd`d-qbEQ1qsgn5H_a`#3l4_K2Vc(;WSqMz-o)O}?w)(Xr}W zosf5L8XXREF7SI)_gLLEXY)SQ;eDzGwX0=>v-eGFJv+@MJ#~b2&|zP1b=pOfAf3c{ zi)~F4*GG7JcfNV>VvwzAC+Boku%2mR`G?hA-P1>a-7i}L3+tzP=7VNHuv0G?Li;4& z=3ZU4J~MJ^{#eYAaqa&W{8;{(XXB@y%UVJ_-&_*($}!L?3_L5z7A37%?~3lM^)}wl zw&nlkL2(Q0$5}ax{1d&@X(KSk>YGLdUUl=D?Q?VR451T!=R(Io|xIudA{(3;q`u=)quZ@FDB2c1EryL0-n#PmX z$yK_}R-xA18Kd&O)vF=kdR&;Zx7zgr+MHnQjtW!-Rl120-nT|H_BvX2a0IGt2*AJ2 z>potWv$Vi|Yp0XwY8hm!cJ;P9yzjAT z$wr>oni`{-Nf(mc3!jh39OjpvTHnv{WJz_|)4;a1Z5L*Lj!WJ7F|+U!&(>{o{Y!NT z0OIZH{&5SuItTPl?vG8Ee2C?h=8J0Td>#cD;~!?pu8->0IpYP@Wk$=5ZR^%0R*mI_ zwFPO9(Z~6+OUc+?fuEyQ<260d;ZmG$fj3u4?2oHl&@;^eG4n=XeD=n{Cds*ggbs** zCcFAVkOBVVPRtHYMj?six~3tE`LTO_Tup74I1#cUUGAA^^SLJ&=F-6&xRBB@_WD zcL#&tReOn6(ngeC0&=|qj2qOsn+wY1mV>~L9H2daz3TB;L+X~)Yljt_N@*{UVrXV5FyVD29!2p z*S&Ylth0Zxu`)AF?25MqH4p>eM4Vq8|CL+ju$PKcg-!R<2+Nqo_{^Jnw~t-7^xc~u zbTjW&;NCX8t+Hj$bs6_0;4y6a^~}@Yq^}Hr_KL~#{QKi&8wVxhVf^#2(gjn@;szzp zGu9Y2D8t5=FhV`k(vaqCfr+ZJ+jTyc%a#)O6;v17M0I6Gk=SWyu|4ApJ*dJa7_G+W z=-4`+TfHFa#ct~r;z*;8@+B`irY5=7kfnbPRvEmyL<`priuE&Eq!*{ zw``Mpto~d`H~%DRM{SIq?YT1|=kT~bw?^EV=~=z|SP+M+*_dWycxGd+zG=zA#fPg+%{K147za4! z1_!yP=iCUvMs<^-u0Mf@k83mNxPx-79v}Ity3WV&%#7yP`pPB^`B3;Sg)`QCeR9wp zm$&=@1?ZU?CxZzZ>$vIwW1W!RK?VL^*iBp*5S8CQ8S4Vw9utiTR%PG>p;v_`f#nil zts@ev$}Z{Mv<@O3ndTfGluS$l?VT-yBHt=boeMC?`y%&(acxsNG(5K2{-%HP%6J;O6WqywLR##I-Z*>*O4oa-t9fni9DKlkgLo0lTY>>cqV*IY)^^t}a_cqf` zX6oF-zCI59L0>)f`pucI#$jfiVmC&LD>>x~Q5;k_dEQOk#PzP?AKX>W=4v7=pBsvu zkYs5NstW9{o0#`%Ky&834i}V#sL|0f(MM>&^~P~liNw?ehcwp91XAz_#1I0E zqPS4&Aa}#V3TOz09MpJC^m_1dz~aOELg!tNNED{AZ$zU1Bw>uEiIpqjzZ#j58GYth zX8Xa+_J6q<=Msa7dgZ1nT zG`|kOSyA4Zjs1h%CtL@5bKy^fULst5&$)HA{XE{L!kaDI2dz^_UdTIaYv*p6JQ2^) zDIuY|Q_{WgtJK=q1DPGVv2k}5n>F8`3A-rTjC!*NBzMix*)^!@5kn&C=OD}yT~o8p zH}k8yg5B=v&_l%F?A=*lJIBY%6he+pLHIK(ya7ePAq1xa!_zK`2fi-56c+}|z#oJ* zE-lE_zj;s6Ik=7Tt6``VMg&YLvhdeC8RH)jsF+91R>AY1t3U-HK2Of*OG9(m@(HY(GIwb@GkJa zbq!GY+uI`O=MGb=BwCdx3Ke0Ct@3q z;F^>mBzYh&CB^3PwGuz>eC}zMWb$nlMGb`Z>kk0I;oFc{!^6JmHlzVU(I0~;*n+(= zbXbN*On*#<;_O|Q%ggY|+|9n0M4)QSIzqz5zQ+N^e0NZVn8@HH@adc4H?ByKeByjr zKHg%igne;-@dMicaSFoG8}dhTSX;F{914m7Hg0dYSox;N<`3EoJKl-?+%xsA1JYu|nWu@KS(%bb zeeMZQH_6LByf0^ffMFtbiEAb2c|QV7sxg0O%jDe6ugCH%2i$UpK3fXhW1ZX@sxrbK zuoW+G9yN}@uMgfnz~3xJTy+F!Ac*W6@z_U{U!1$t-RiLqJ|KB)V%`CSLp~uDE}Y8o z4kH#KJ`NjT)YY9g4$|wa_V-T<{bg9#`L8A@`ckSYaGDvO`F5gDsa@Q4@r>ExSO2iWo(;gh`8`FKlOr}Mb_dYrFi zk(l3HGJNkFFX%&k6sO%EkHE!1!egn->t^Wu>-MgTVlQv)?gt)#6BYfO+HCY zXX{a7$D<52M}N9VO<{S{@U}i3y<#^MpUP@-n0U&)P~+aG`CnPtcb|Ug&nWXKQl0o` z7fjx%G_`hX=n4I{@~3_Adwl~)4=69;5@jg4N@CZF?_j*6P zp?=e|f2{7ferSNl{Hr%Ecb@fS;&yMBZw^HG&o{7J(!*?Qf~iZQ#n1!`*TnG;ZbbBO z_Rw+ma8q6Jh#VUbtTXn(Q1$o(6BoX(d@$#O%^n?nTne)qAH=*UvT*fq`Y~zzgHu;C z(p^>khS$yE*DPIUM%vibWu)sZxe}KiXIT|jV3B0XHOHzgGPd>{8=&K2=%&*6-qN)R z&~e_fp}&`xS5)nKV;%jH4YhY~EO2xjZl_*w#Un5RzRac%2sn$4`+^xMbHZSHEe zyehBhLy!8dG0uj8ch~p#(wp0)?%3F|p(B2F+r~Pxb*B~;){dE*-E`uk?3q@UQTE*T zOr7wfjLz;sRk>^5GS@A8y{cSq_Y60_!u{zzhzU0i)?^d}-2JGJi-)DFDspT>T82tq zN=q6aaK%ICMOXq?3GWfwqdD-woW5fpFr18Z9$ZyAFRU^OUKNd3AKUDq%zK|v!hFmqf2JDsbfou?+$N2aq?s*v-g5^wi-JcI%ZRr9Q;(7K@k+_f`Wvt~qrKj@+0EqnFU|H1(`$ z>ayo@KTTh#WJbi18Z=dCer}F)Ty!|h)zJ72puE2Ec zk{-yyEOd5yQd*L~nVGZ7?Kb0CDSz|xE@QN&6U-KCN4QSGkc7+yVImw+}q5rdHBTo*@d;;&0GH6rI$I% zEg!8;^NLexau{oX=Fq)36-K2|S;l!-mOm{raaL6(^iOIjikfA)Zu(4(huf{5%|+ac z(~nq~OP>-3zx8L2e!~NLIIC)muVknqc}XWU*o_Z$PK=tR!Eoy4F!EbHvH4$y*1b+h zi%WOS>En`^#uaQH3>CL9v$0c)Q@bP{+wXB@=!1h9Jn!q0oroEExFsg04YgATn3;)1 z-|dPnH)geLT;8ItY1S>SH?vFFJ2YYMANqFa+o!x!mRE~af;1Ikw#_bB;jOWk^lr2a|vbzVc&X$(nKskY$D zDYb=Iy^ag^ce~6(0wr4_h4E2 z(1f(SnLQY`8QR-8uCUGw(DG~6^=ry%=~~mYQQyqiIym@|ad70$`kpR{iL;~2YMLY4 z)H4`$REhq?`t@DRqx=WR$Qih6b{`w{0;^LWZ86bz&e_smFJXa3Pi-Gyh4cEa&h68j zvw@-OIIDWt)XkZxQ#Lvw;D^S#K$|6+${Vi040oTFgc-E^cxd7}j)G$?UMev8&TO~n zUtG5!sVgt9`ffRfDTHgPCiV)B3=h_M@k@pWj%Z3yz%9fx68E}#$dhW^vxm!y-Y$K| zCIp~akKi6<8^#6uM3pkijmx7-1*@ATtSeoPs-wz=-&t|@ z6xr6bxOTbtY_0CdsFF_l`Xfcn8%*>|MyvaKxo+8D0=5s(OW0si^3hC7y46nqgEO9qr0B}ue`v#exJFiDggkGbiD@$v5f?aDxOOyOUH>_W(EtWEC2TL{a7(t zi@Ys+T@wQ=@!&*LGZ&8@W#x8u);-LY=dHNg6%h^irEhpd6a4 z66_OG!1VjO-YSc;Op5BIi6hvEvirNv#1b0HQa5{Od{u5UBVD5XX1d+FiYYvZ!xN0J z_;nAq3U-_6w#6a?&Ce9UV6u7-0>iPEWBXtB9IM`&k)Bb2tL!b02nG+(J;5wET}&Vs zq2TKZV+fcaPHdNus9%DMYY`h;2eI2v5WL1kmkz(vlrXLk=eM&C?$MP{=WNI^d5%P` z_o7egn@5!l!2(j7F#0hQ>$~%PC;j)Vbl$VFV&210=Zqefj~M{#i#AJYeJ>qNnzU82d@ z1bd0q*ajg`MP!mdKA!jkS$Qz$_ElAn@Y$9Uw#C7HRlz#pS2A)iftfvARKYM3v+AJ< z0e43yB;p-=QM>|U%x{$>DXS`s2~n|XzJpLh)HJLP)oYL(uDs-(0Gk1HNM!o?uckyNh7l`7X9 zk93x5i9`|t(lc_zN}w_xK0~d2s;Y1dSX|-Vp2pVErRCtFe6Pj9;NajM_|TPDy*k2~ z9^^$qK6CIu33OgjZ+!;gY}g>dPkiL=`VC#w8~D>-a04-lRao`VXKI(Ih%IR1a{7c? zxgi%rw=H>_iDZWGi~w9@13=*hGw9P?Wa457HPZODeVYccA>Qi>JuOJfuv|Cowyq1f zFqBa8Akbyz6;ry>Lukca{YG_>72bV;yw^f@`e zqp_XxM=J(=q;s2qYrvgMdN=Lg&Hg)2oIe675gKT0oExtN@yT*_a`U9{jRt z_?6f=xU$CA(9IB6+0frraRCvH^A`W!&W18p<)8lm%!a38ISO6quMMAS|K9pG;HVgc z+Md6HmcD)9OGA6Xj)XWYb4akS)8qe~Ef#yqGF0#%0EDMQ6HLfo4hm6y1#$A|w?DlR z?H|+U&V<~$^bJ3H`>L;pxPVY9kHB=j+|b^HbN&P{#-Ry(WoWoW6+CAsLrlk0)EU?mO!4OUm zfVImP9N#_xIIs^8U=Rwd@!b+IuXnhPb7FrPPukGrMWLiEu2}v|f**+;uaeI+!uKtR z?^h((<1eWjw8kZI{1Wbv9N@tmMY*I*@H(tq?7mze02V~ZBq~S_NodqQ;v4C_Nudt% zBa9^c%IgOmaZbklHWR>2LgnUBj<~3ajkrHJ#WUr_BzeUW70tnY3tGJTxbz7%j%>Et zQ>LYe_uZ$WkRwQC)c%68WNt(fB5sgkSOf zU^rRpFYBgx0Q`*)4!ZUoyW|RLOf(JW_MkmU_%|={%oUYz*UHn|S5H0YFp9M%!`-|H z|B<*`V9z}RC5(Uvw?3g7Rb;5

      WaL$ceo=^3vk^b?>)%YguJ{@n8{cTG7Zmvn#o^ zzRqvLu2lY9@GA60;DgGZ&Tn?VDq1rO5+Zn;&I2h$I9Y={$Sv|p!J1i`j3;riJ~K5h z%lHJx%>_s$%Fl5#!kE*i*{YF9R~P8a-NhEQthPyLS?#Rqc%l2kZxRmtCeI?H_{Vme zaGe+R4+3B0@CkdadPIsgVn}r{>_sogi-A-cSbA# From 64bc60d3d8796b0c7a3d0ff570a1368df246c850 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 1 Nov 2023 22:02:13 +0100 Subject: [PATCH 31/64] [mybmw] revert setting default state unit in channel-type definition Signed-off-by: Martin Grassl --- .../OH-INF/thing/location-channel-types.xml | 2 +- .../OH-INF/thing/range-channel-types.xml | 16 ++++++++-------- .../OH-INF/thing/service-channel-types.xml | 2 +- .../OH-INF/thing/tires-channel-types.xml | 16 ++++++++-------- .../thing/vehicle-status-channel-types.xml | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml index 28959b3776253..df395e8a4cf01 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/location-channel-types.xml @@ -20,6 +20,6 @@ Number:Length Computed distance between vehicle and home location - + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml index 1b9a5eb1a7a4b..81adde871f35a 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml @@ -6,22 +6,22 @@ Number:Length - + Number:Length - + Number:Length - + Number:Length - + Number:Dimensionless @@ -31,7 +31,7 @@ Number:Volume - + Number:Dimensionless @@ -46,16 +46,16 @@ Number:Length - + Number:Length - + Number:Length - + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml index ac2727b6b8c8c..827dd8a808b83 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/service-channel-types.xml @@ -19,6 +19,6 @@ Number:Length - + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml index 361c9d4bb5eb0..e403c049fafd7 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/tires-channel-types.xml @@ -6,41 +6,41 @@ Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + Number:Pressure - + diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml index 5ffe5974cbda8..cc0bf7f5d97a9 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml @@ -25,7 +25,7 @@ Number:Length - + String From f1d60cf42f795ac6f1f860fd9b1ab1936af561c2 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 27 Nov 2023 22:05:31 +0100 Subject: [PATCH 32/64] [mybmw] cleanup pom file Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/pom.xml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/pom.xml b/bundles/org.openhab.binding.mybmw/pom.xml index 079ee4d35c3d7..521d30c808daa 100644 --- a/bundles/org.openhab.binding.mybmw/pom.xml +++ b/bundles/org.openhab.binding.mybmw/pom.xml @@ -14,17 +14,6 @@ openHAB Add-ons :: Bundles :: MyBMW Binding - - - - org.jacoco - org.jacoco.agent - runtime - 0.8.8 - test - - - test-coverage @@ -93,6 +82,17 @@ + + + + + org.jacoco + org.jacoco.agent + runtime + 0.8.8 + test + + @@ -29,7 +29,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -65,7 +65,7 @@ - + From ced799e893196737b759eeef95d21c915c57ff0f Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 11 Dec 2023 22:25:25 +0100 Subject: [PATCH 39/64] [mybmw] fix command for start charging Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/handler/enums/RemoteService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java index a8ecff69f97d3..0436c2555ea6c 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/enums/RemoteService.java @@ -39,7 +39,7 @@ public enum RemoteService { HORN_BLOW("Horn Blow", REMOTE_SERVICE_HORN, REMOTE_SERVICE_HORN, ""), CLIMATE_NOW_START("Start Climate", REMOTE_SERVICE_AIR_CONDITIONING_START, "climate-now", "{\"action\": \"START\"}"), CLIMATE_NOW_STOP("Stop Climate", REMOTE_SERVICE_AIR_CONDITIONING_STOP, "climate-now", "{\"action\": \"STOP\"}"), - CHARGE_NOW("Charge", REMOTE_SERVICE_CHARGE, "charge-now", ""); + CHARGE_NOW("Charge", REMOTE_SERVICE_CHARGE, "start-charging", ""); private final String label; private final String id; From c3fa9cfcd770ff42dfbe21ce1b8b47342287c1e7 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Mon, 11 Dec 2023 22:40:12 +0100 Subject: [PATCH 40/64] [mybmw] fix FindBugs logging error Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java index 549c78a8c8784..41e0fa503e699 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -149,7 +149,7 @@ public List requestVehiclesBase() throws NetworkException { Thread.sleep(10000); } catch (Exception e) { - logger.error("error retrieving the base vehicles ", e.getMessage()); + logger.error("error retrieving the base vehicles {}", e.getMessage()); } } From 96be12e8bb213f82c1beb8dfebfc5364c6342d21 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 12 Dec 2023 21:54:02 +0100 Subject: [PATCH 41/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/console/MyBMWCommandExtension.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java index a19775b3a82f5..8fec525ee08eb 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java @@ -203,7 +203,7 @@ public void execute(String[] args, Console console) { deleteDirectory(path); console.println("### Fingerprint has been written to zipfile: " + zipfile); } catch (IOException e) { - console.println("Exception zipping fingerprint " + e.getMessage()); + console.println("Exception zipping fingerprint: " + e.getMessage()); console.println("### Fingerprint has been written to files in directory: " + path); } From 56ff217f4ce4770451beffd042afedd9edb7f785 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 12 Dec 2023 21:54:23 +0100 Subject: [PATCH 42/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/console/MyBMWCommandExtension.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java index 8fec525ee08eb..370e92b90a1e4 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/console/MyBMWCommandExtension.java @@ -216,7 +216,7 @@ private void printAndSave(Console console, String path, String filename, String try { writeJsonToFile(path, filename, json); } catch (IOException e) { - console.println("Exception writing to file " + e.getMessage()); + console.println("Exception writing to file: " + e.getMessage()); } } From a31a767981b4547646aa1607186c939bbc52f9f0 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 12 Dec 2023 22:02:46 +0100 Subject: [PATCH 43/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- .../openhab/binding/mybmw/internal/handler/VehicleHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java index 90ac3fedec5fb..15be34f7950a9 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java @@ -275,7 +275,7 @@ private void startSchedule(int interval) { @Override public void dispose() { - logger.trace("VehicleHandler.idispose"); + logger.trace("VehicleHandler.dispose"); refreshJob.ifPresent(job -> job.cancel(true)); editTimeout.ifPresent(job -> job.cancel(true)); remote.ifPresent(RemoteServiceExecutor::cancel); From 89c31d1db566b3c90b29b0576b57cb87c3049819 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Tue, 12 Dec 2023 22:20:44 +0100 Subject: [PATCH 44/64] [mybmw] fixed some small things due to code review Signed-off-by: Martin Grassl --- .../mybmw/internal/discovery/VehicleDiscovery.java | 10 +++++++--- .../mybmw/internal/handler/backend/MyBMWHttpProxy.java | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java index 91e2a6251787d..b0bee75db1467 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java @@ -64,9 +64,9 @@ public VehicleDiscovery() { @Override public void setThingHandler(ThingHandler handler) { - if (handler instanceof MyBMWBridgeHandler) { + if (handler instanceof MyBMWBridgeHandler bmwBridgeHandler) { logger.trace("VehicleDiscovery.setThingHandler for MybmwBridge"); - bridgeHandler = Optional.of((MyBMWBridgeHandler) handler); + bridgeHandler = Optional.of(bmwBridgeHandler); bridgeHandler.get().setVehicleDiscovery(this); bridgeUid = Optional.of(bridgeHandler.get().getThing().getUID()); } @@ -115,6 +115,9 @@ public void discoverVehicles() { /** * this method is called by the bridgeHandler if the list of vehicles was retrieved successfully * + * it iterates through the list of existing things and checks if the vehicles found via the API + * call are already known to OH. If not, it creates a new thing and puts it into the inbox + * * @param vehicleList */ private void processVehicles(List vehicleList) { @@ -137,7 +140,7 @@ private void processVehicles(List vehicleList) { List vehicleThings = bridgeHandler.get().getThing().getThings(); for (Thing vehicleThing : vehicleThings) { Configuration configuration = vehicleThing.getConfiguration(); - // boolean thingFound = true; + if (configuration.containsKey(MyBMWConstants.VIN)) { String thingVIN = configuration.get(MyBMWConstants.VIN).toString(); if (vehicle.getVehicleBase().getVin().equals(thingVIN)) { @@ -147,6 +150,7 @@ private void processVehicles(List vehicleList) { } } + // the vehicle found is not yet known to OH, so put it into the inbox if (!thingFound) { // Properties needed for functional Thing VehicleAttributes vehicleAttributes = vehicle.getVehicleBase().getAttributes(); diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java index 41e0fa503e699..b001f58034cd6 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -149,7 +149,7 @@ public List requestVehiclesBase() throws NetworkException { Thread.sleep(10000); } catch (Exception e) { - logger.error("error retrieving the base vehicles {}", e.getMessage()); + logger.warn("error retrieving the base vehicles for brand {}: {}", brand, e.getMessage()); } } From 852f2752f91353a674018a50d661f4d87e0ed5e8 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 12:41:07 +0100 Subject: [PATCH 45/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/dto/vehicle/VehicleLocation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java index 04b55ff5fbc9e..655fe226ee612 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleLocation.java @@ -49,6 +49,6 @@ public void setHeading(int heading) { @Override public String toString() { - return "Location [coordinates=" + coordinates + ", address=" + address + ", heading=" + heading + "]"; + return "VehicleLocation [coordinates=" + coordinates + ", address=" + address + ", heading=" + heading + "]"; } } From c397b600df98c21dcc9cf65ab46c5c97216e689d Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 12:42:38 +0100 Subject: [PATCH 46/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/handler/MyBMWBridgeHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java index e3b2d4c29b213..894a48c0c10a2 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java @@ -145,7 +145,7 @@ private void discoverVehicles() { @Override public Collection> getServices() { logger.trace("MyBMWBridgeHandler.getServices"); - return Collections.singleton(VehicleDiscovery.class); + return List.of(VehicleDiscovery.class); } public Optional getMyBmwProxy() { From c3389d43499706d8c6105df3936b31500c7d5d7d Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 12:47:16 +0100 Subject: [PATCH 47/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- .../openhab/binding/mybmw/internal/handler/VehicleHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java index 15be34f7950a9..b68aff6c62961 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java @@ -363,8 +363,7 @@ private void updateChannel(final String group, final String id, final String sta /** * this method sets the state for a single channel. if a channelToBeUpdated is provided, the update will only take - * place for that - * single channel + * place for that single channel. */ private void updateChannel(final String group, final String id, final State state, @Nullable final String channelToBeUpdated) { From b8757add6387c0b6fb48dbdeef2bdaf99b15476c Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 12:48:14 +0100 Subject: [PATCH 48/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- .../openhab/binding/mybmw/internal/handler/VehicleHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java index b68aff6c62961..15b304a1822c4 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/VehicleHandler.java @@ -524,8 +524,7 @@ private void updateRange(VehicleState vehicleState, @Nullable String channelToBe int rangeCombined = vehicleState.getRange(); // there is a bug/feature in the API that the fuel range is the same like the combined range, hence in case - // of - // hybrid the fuel range has to be subtracted by the electric range + // of hybrid the fuel range has to be subtracted by the electric range int rangeFuel = vehicleState.getCombustionFuelLevel().getRange() - vehicleState.getElectricChargingState().getRange(); From 53c1dd15fb2737c3ea03b49dcec37e139c76f78d Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 12:49:19 +0100 Subject: [PATCH 49/64] Update bundles/org.openhab.binding.mybmw/README.md Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md index 085b970950a71..04b7d270d56be 100644 --- a/bundles/org.openhab.binding.mybmw/README.md +++ b/bundles/org.openhab.binding.mybmw/README.md @@ -163,7 +163,7 @@ Reflects overall status of the vehicle. | Check Control | check-control | String | Presence of active warning messages | X | X | X | X | | Plug Connection Status | plug-connection | String | Plug is _Connected_ or _Not connected_ | | X | X | X | | Charging Status | charge | String | Current charging status | | X | X | X | -| Remaining Charging Time | charge-remaining | Number:Time | Remainining time for current charging session | | X | X | X | +| Remaining Charging Time | charge-remaining | Number:Time | Remaining time for current charging session | | X | X | X | | Last Status Timestamp | last-update | DateTime | Date and time of last status update | X | X | X | X | | Last Fetched Timestamp | last-fetched | DateTime | Date and time of last time status fetched | X | X | X | X | From 5ed0773f132baac74d44104a3594020c22f40b81 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 12:50:24 +0100 Subject: [PATCH 50/64] Update bundles/org.openhab.binding.mybmw/README.md Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- bundles/org.openhab.binding.mybmw/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md index 04b7d270d56be..b8ba468f546da 100644 --- a/bundles/org.openhab.binding.mybmw/README.md +++ b/bundles/org.openhab.binding.mybmw/README.md @@ -535,7 +535,8 @@ Your feedback is highly appreciated! #### Debug Logging -You can [enable debug logging](https://www.openhab.org/docs/administration/logging.html#defining-what-to-log) to get more information on the behaviour of the binding. The package.subpackage in this case would be "org.openhab.binding.mybmw". +You can [enable debug logging](https://www.openhab.org/docs/administration/logging.html#defining-what-to-log) to get more information on the behaviour of the binding. +The package.subpackage in this case would be "org.openhab.binding.mybmw". As with fingerprint data, personal data is eliminated from logs. From 06ca7afd6cb54943a7f8a737bf7badfb63b97869 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 12:51:17 +0100 Subject: [PATCH 51/64] Update bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java Co-authored-by: Jacob Laursen Signed-off-by: Martin Grassl --- .../mybmw/internal/handler/auth/MyBMWTokenController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java index f6c3996a58041..759ec4e468479 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java @@ -136,7 +136,7 @@ public Token getToken() { } /** - * Everything is catched by surroundig try catch + * Everything is caught by surrounding try catch * - HTTP Exceptions * - JSONSyntax Exceptions * - potential NullPointer Exceptions From bc8ae1562cf6b5c1749c573435bb4a03231edff5 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 21:17:59 +0100 Subject: [PATCH 52/64] [mybmw] fix compile error Signed-off-by: Martin Grassl --- .../binding/mybmw/internal/handler/MyBMWBridgeHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java index 894a48c0c10a2..ab63352c39ec1 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/MyBMWBridgeHandler.java @@ -13,7 +13,7 @@ package org.openhab.binding.mybmw.internal.handler; import java.util.Collection; -import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; From 1c9325b02ce30caf146a663125c63cd176dfd0a6 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 21:28:46 +0100 Subject: [PATCH 53/64] [mybmw] extract strings to constants Signed-off-by: Martin Grassl --- .../mybmw/internal/MyBMWConstants.java | 20 +++++++++++++++++++ .../internal/discovery/VehicleDiscovery.java | 20 +++++++++---------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java index 36d3dcd20681f..e8c5afe000d20 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/MyBMWConstants.java @@ -36,6 +36,26 @@ public interface MyBMWConstants { static final String VEHICLE_BRAND = "vehicleBrand"; + static final String REMOTE_SERVICES_DISABLED = "remoteServicesDisabled"; + + static final String REMOTE_SERVICES_ENABLED = "remoteServicesEnabled"; + + static final String SERVICES_DISABLED = "servicesDisabled"; + + static final String SERVICES_ENABLED = "servicesEnabled"; + + static final String SERVICES_UNSUPPORTED = "servicesUnsupported"; + + static final String SERVICES_SUPPORTED = "servicesSupported"; + + static final String VEHICLE_BODYTYPE = "vehicleBodytype"; + + static final String VEHICLE_CONSTRUCTION_YEAR = "vehicleConstructionYear"; + + static final String VEHICLE_DRIVE_TRAIN = "vehicleDriveTrain"; + + static final String VEHICLE_MODEL = "vehicleModel"; + static final int DEFAULT_IMAGE_SIZE_PX = 1024; static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 5; diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java index b0bee75db1467..f2b8bdbf66c08 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/discovery/VehicleDiscovery.java @@ -175,20 +175,20 @@ private Map generateProperties(Vehicle vehicle) { // Vehicle Properties VehicleAttributes vehicleAttributes = vehicle.getVehicleBase().getAttributes(); - properties.put("vehicleModel", vehicleAttributes.getModel()); - properties.put("vehicleDriveTrain", vehicleAttributes.getDriveTrain()); - properties.put("vehicleConstructionYear", Integer.toString(vehicleAttributes.getYear())); - properties.put("vehicleBodytype", vehicleAttributes.getBodyType()); + properties.put(MyBMWConstants.VEHICLE_MODEL, vehicleAttributes.getModel()); + properties.put(MyBMWConstants.VEHICLE_DRIVE_TRAIN, vehicleAttributes.getDriveTrain()); + properties.put(MyBMWConstants.VEHICLE_CONSTRUCTION_YEAR, Integer.toString(vehicleAttributes.getYear())); + properties.put(MyBMWConstants.VEHICLE_BODYTYPE, vehicleAttributes.getBodyType()); VehicleCapabilities vehicleCapabilities = vehicle.getVehicleState().getCapabilities(); - properties.put("servicesSupported", + properties.put(MyBMWConstants.SERVICES_SUPPORTED, vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.SUPPORTED_SUFFIX, true)); - properties.put("servicesUnsupported", + properties.put(MyBMWConstants.SERVICES_UNSUPPORTED, vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.SUPPORTED_SUFFIX, false)); - properties.put("servicesEnabled", + properties.put(MyBMWConstants.SERVICES_ENABLED, vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.ENABLED_SUFFIX, true)); - properties.put("servicesDisabled", + properties.put(MyBMWConstants.SERVICES_DISABLED, vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.ENABLED_SUFFIX, false)); // For RemoteServices we need to do it step-by-step @@ -224,8 +224,8 @@ private Map generateProperties(Vehicle vehicle) { } else { remoteServicesDisabled.append(RemoteService.CLIMATE_NOW_START.getLabel() + Constants.SEMICOLON); } - properties.put("remoteServicesEnabled", remoteServicesEnabled.toString().trim()); - properties.put("remoteServicesDisabled", remoteServicesDisabled.toString().trim()); + properties.put(MyBMWConstants.REMOTE_SERVICES_ENABLED, remoteServicesEnabled.toString().trim()); + properties.put(MyBMWConstants.REMOTE_SERVICES_DISABLED, remoteServicesDisabled.toString().trim()); return properties; } From 50857a203958aa0beb1a902e967ab9eea1052e82 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 21:30:20 +0100 Subject: [PATCH 54/64] [mybmw] cleanup comments Signed-off-by: Martin Grassl --- .../dto/vehicle/VehicleCapabilities.java | 171 +++++++++--------- 1 file changed, 81 insertions(+), 90 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java index b90a87f9b1115..7241dafe13afa 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/dto/vehicle/VehicleCapabilities.java @@ -35,49 +35,54 @@ public class VehicleCapabilities { public static final String SUPPORTED_SUFFIX = "Supported"; public static final String ENABLED_SUFFIX = "Enabled"; - // private boolean remoteChargingCommands = false; // {}, don't know what comes - // private boolean specialThemeSupport = false; // [] don't know what comes here - private boolean checkSustainabilityDPP = false; // false, - private boolean climateNow = false; // true, - private boolean horn = false; // true, - private boolean isBmwChargingSupported = false; // false, - private boolean isCarSharingSupported = false; // false, - private boolean isChargeNowForBusinessSupported = false; // false, - private boolean isChargingHistorySupported = false; // false, - private boolean isChargingHospitalityEnabled = false; // false, - private boolean isChargingLoudnessEnabled = false; // false, - private boolean isChargingPlanSupported = false; // false, - private boolean isChargingPowerLimitEnabled = false; // false, - private boolean isChargingSettingsEnabled = false; // false, - private boolean isChargingTargetSocEnabled = false; // false, - private boolean isClimateTimerSupported = false; // true, - private boolean isClimateTimerWeeklyActive = false; // true, - private boolean isCustomerEsimSupported = false; // false, - private boolean isDataPrivacyEnabled = false; // false, - private boolean isDCSContractManagementSupported = false; // false, - private boolean isEasyChargeEnabled = false; // false, - private boolean isEvGoChargingSupported = false; // false, - private boolean isMiniChargingSupported = false; // false, - private boolean isNonLscFeatureEnabled = false; // false, - private boolean isRemoteEngineStartSupported = false; // false, - private boolean isRemoteHistoryDeletionSupported = false; // false, - private boolean isRemoteHistorySupported = false; // true, - private boolean isRemoteParkingSupported = false; // false, - private boolean isRemoteServicesActivationRequired = false; // false, - private boolean isRemoteServicesBookingRequired = false; // false, - private boolean isScanAndChargeSupported = false; // false, - private boolean isSustainabilityAccumulatedViewEnabled = false; // false, - private boolean isSustainabilitySupported = false; // false, - private boolean isWifiHotspotServiceSupported = false; // true, - private boolean lights = false; // true, - private boolean lock = false; // true, + private boolean checkSustainabilityDPP = false; + private boolean climateNow = false; + private boolean horn = false; + private boolean isBmwChargingSupported = false; + private boolean isCarSharingSupported = false; + private boolean isChargeNowForBusinessSupported = false; + private boolean isChargingHistorySupported = false; + private boolean isChargingHospitalityEnabled = false; + private boolean isChargingLoudnessEnabled = false; + private boolean isChargingPlanSupported = false; + private boolean isChargingPowerLimitEnabled = false; + private boolean isChargingSettingsEnabled = false; + private boolean isChargingTargetSocEnabled = false; + private boolean isClimateTimerSupported = false; + private boolean isClimateTimerWeeklyActive = false; + private boolean isCustomerEsimSupported = false; + private boolean isDataPrivacyEnabled = false; + private boolean isDCSContractManagementSupported = false; + private boolean isEasyChargeEnabled = false; + private boolean isEvGoChargingSupported = false; + private boolean isMiniChargingSupported = false; + private boolean isNonLscFeatureEnabled = false; + private boolean isRemoteEngineStartSupported = false; + private boolean isRemoteHistoryDeletionSupported = false; + private boolean isRemoteHistorySupported = false; + private boolean isRemoteParkingSupported = false; + private boolean isRemoteServicesActivationRequired = false; + private boolean isRemoteServicesBookingRequired = false; + private boolean isScanAndChargeSupported = false; + private boolean isSustainabilityAccumulatedViewEnabled = false; + private boolean isSustainabilitySupported = false; + private boolean isWifiHotspotServiceSupported = false; + private boolean lights = false; + private boolean lock = false; private boolean remote360 = false; private RemoteChargingCommands remoteChargingCommands = new RemoteChargingCommands(); - private boolean remoteSoftwareUpgrade = false; // true, - private boolean sendPoi = false; // true, - private boolean speechThirdPartyAlexa = false; // true, - private boolean speechThirdPartyAlexaSDK = false; // false, - private boolean unlock = false; // true, + private boolean remoteSoftwareUpgrade = false; + private boolean sendPoi = false; + private boolean speechThirdPartyAlexa = false; + private boolean speechThirdPartyAlexaSDK = false; + private boolean unlock = false; + private boolean vehicleFinder = false; + private DigitalKey digitalKey = new DigitalKey(); + private String a4aType = ""; // NOT_SUPPORTED, + private String climateFunction = ""; // VENTILATION, + private String climateTimerTrigger = ""; // DEPARTURE_TIMER, + private String lastStateCallState = ""; // ACTIVATED, + private String vehicleStateSource = ""; // LAST_STATE_CALL, /** * @return the climateNow @@ -142,58 +147,12 @@ public DigitalKey getDigitalKey() { return digitalKey; } - private boolean vehicleFinder = false; // true, - private DigitalKey digitalKey = new DigitalKey(); - private String a4aType = ""; // NOT_SUPPORTED, - private String climateFunction = ""; // VENTILATION, - private String climateTimerTrigger = ""; // DEPARTURE_TIMER, - private String lastStateCallState = ""; // ACTIVATED, - private String vehicleStateSource = ""; // LAST_STATE_CALL, - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - - @Override - public String toString() { - return "VehicleCapabilities [checkSustainabilityDPP=" + checkSustainabilityDPP + ", climateNow=" + climateNow - + ", horn=" + horn + ", isBmwChargingSupported=" + isBmwChargingSupported + ", isCarSharingSupported=" - + isCarSharingSupported + ", isChargeNowForBusinessSupported=" + isChargeNowForBusinessSupported - + ", isChargingHistorySupported=" + isChargingHistorySupported + ", isChargingHospitalityEnabled=" - + isChargingHospitalityEnabled + ", isChargingLoudnessEnabled=" + isChargingLoudnessEnabled - + ", isChargingPlanSupported=" + isChargingPlanSupported + ", isChargingPowerLimitEnabled=" - + isChargingPowerLimitEnabled + ", isChargingSettingsEnabled=" + isChargingSettingsEnabled - + ", isChargingTargetSocEnabled=" + isChargingTargetSocEnabled + ", isClimateTimerSupported=" - + isClimateTimerSupported + ", isClimateTimerWeeklyActive=" + isClimateTimerWeeklyActive - + ", isCustomerEsimSupported=" + isCustomerEsimSupported + ", isDataPrivacyEnabled=" - + isDataPrivacyEnabled + ", isDCSContractManagementSupported=" + isDCSContractManagementSupported - + ", isEasyChargeEnabled=" + isEasyChargeEnabled + ", isEvGoChargingSupported=" - + isEvGoChargingSupported + ", isMiniChargingSupported=" + isMiniChargingSupported - + ", isNonLscFeatureEnabled=" + isNonLscFeatureEnabled + ", isRemoteEngineStartSupported=" - + isRemoteEngineStartSupported + ", isRemoteHistoryDeletionSupported=" - + isRemoteHistoryDeletionSupported + ", isRemoteHistorySupported=" + isRemoteHistorySupported - + ", isRemoteParkingSupported=" + isRemoteParkingSupported + ", isRemoteServicesActivationRequired=" - + isRemoteServicesActivationRequired + ", isRemoteServicesBookingRequired=" - + isRemoteServicesBookingRequired + ", isScanAndChargeSupported=" + isScanAndChargeSupported - + ", isSustainabilityAccumulatedViewEnabled=" + isSustainabilityAccumulatedViewEnabled - + ", isSustainabilitySupported=" + isSustainabilitySupported + ", isWifiHotspotServiceSupported=" - + isWifiHotspotServiceSupported + ", lights=" + lights + ", lock=" + lock + ", remote360=" + remote360 - + ", remoteChargingCommands=" + remoteChargingCommands + ", remoteSoftwareUpgrade=" - + remoteSoftwareUpgrade + ", sendPoi=" + sendPoi + ", speechThirdPartyAlexa=" + speechThirdPartyAlexa - + ", speechThirdPartyAlexaSDK=" + speechThirdPartyAlexaSDK + ", unlock=" + unlock + ", vehicleFinder=" - + vehicleFinder + ", digitalKey=" + digitalKey + ", a4aType=" + a4aType + ", climateFunction=" - + climateFunction + ", climateTimerTrigger=" + climateTimerTrigger + ", lastStateCallState=" - + lastStateCallState + ", vehicleStateSource=" + vehicleStateSource + "]"; - } - /** * returns a list of capabilities filtered by the provided suffix and the enabled requirement * - * @param suffix - * @param enabled - * @return + * @param suffix the suffix of the capability + * @param enabled if it should return only enabled or disabled capabilities + * @return the list of capabilities as single string */ public String getCapabilitiesAsString(String suffix, boolean enabled) { StringBuffer capabilitiesAsString = new StringBuffer(); @@ -230,4 +189,36 @@ private List getCapabilitiesAsStringList(String suffix, boolean compare) return l; } + + @Override + public String toString() { + return "VehicleCapabilities [checkSustainabilityDPP=" + checkSustainabilityDPP + ", climateNow=" + climateNow + + ", horn=" + horn + ", isBmwChargingSupported=" + isBmwChargingSupported + ", isCarSharingSupported=" + + isCarSharingSupported + ", isChargeNowForBusinessSupported=" + isChargeNowForBusinessSupported + + ", isChargingHistorySupported=" + isChargingHistorySupported + ", isChargingHospitalityEnabled=" + + isChargingHospitalityEnabled + ", isChargingLoudnessEnabled=" + isChargingLoudnessEnabled + + ", isChargingPlanSupported=" + isChargingPlanSupported + ", isChargingPowerLimitEnabled=" + + isChargingPowerLimitEnabled + ", isChargingSettingsEnabled=" + isChargingSettingsEnabled + + ", isChargingTargetSocEnabled=" + isChargingTargetSocEnabled + ", isClimateTimerSupported=" + + isClimateTimerSupported + ", isClimateTimerWeeklyActive=" + isClimateTimerWeeklyActive + + ", isCustomerEsimSupported=" + isCustomerEsimSupported + ", isDataPrivacyEnabled=" + + isDataPrivacyEnabled + ", isDCSContractManagementSupported=" + isDCSContractManagementSupported + + ", isEasyChargeEnabled=" + isEasyChargeEnabled + ", isEvGoChargingSupported=" + + isEvGoChargingSupported + ", isMiniChargingSupported=" + isMiniChargingSupported + + ", isNonLscFeatureEnabled=" + isNonLscFeatureEnabled + ", isRemoteEngineStartSupported=" + + isRemoteEngineStartSupported + ", isRemoteHistoryDeletionSupported=" + + isRemoteHistoryDeletionSupported + ", isRemoteHistorySupported=" + isRemoteHistorySupported + + ", isRemoteParkingSupported=" + isRemoteParkingSupported + ", isRemoteServicesActivationRequired=" + + isRemoteServicesActivationRequired + ", isRemoteServicesBookingRequired=" + + isRemoteServicesBookingRequired + ", isScanAndChargeSupported=" + isScanAndChargeSupported + + ", isSustainabilityAccumulatedViewEnabled=" + isSustainabilityAccumulatedViewEnabled + + ", isSustainabilitySupported=" + isSustainabilitySupported + ", isWifiHotspotServiceSupported=" + + isWifiHotspotServiceSupported + ", lights=" + lights + ", lock=" + lock + ", remote360=" + remote360 + + ", remoteChargingCommands=" + remoteChargingCommands + ", remoteSoftwareUpgrade=" + + remoteSoftwareUpgrade + ", sendPoi=" + sendPoi + ", speechThirdPartyAlexa=" + speechThirdPartyAlexa + + ", speechThirdPartyAlexaSDK=" + speechThirdPartyAlexaSDK + ", unlock=" + unlock + ", vehicleFinder=" + + vehicleFinder + ", digitalKey=" + digitalKey + ", a4aType=" + a4aType + ", climateFunction=" + + climateFunction + ", climateTimerTrigger=" + climateTimerTrigger + ", lastStateCallState=" + + lastStateCallState + ", vehicleStateSource=" + vehicleStateSource + "]"; + } } From dbf9d8d2eb7c0baa8f36526d1767ec201922ae50 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 21:32:04 +0100 Subject: [PATCH 55/64] [mybmw] add description of return value Signed-off-by: Martin Grassl --- .../mybmw/internal/handler/auth/MyBMWTokenController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java index 759ec4e468479..9164c38e019ee 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/auth/MyBMWTokenController.java @@ -141,7 +141,7 @@ public Token getToken() { * - JSONSyntax Exceptions * - potential NullPointer Exceptions * - * @return + * @return true if the token was successfully updated */ private synchronized boolean updateToken() { try { From c8bf8bfe6e7ed1095d41e368dc69ddf062b41c17 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 21:54:29 +0100 Subject: [PATCH 56/64] [mybmw] fix Javadoc Signed-off-by: Martin Grassl --- .../handler/backend/MyBMWHttpProxy.java | 89 +++++++++++++++---- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java index b001f58034cd6..2f1570ba1fb03 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -102,6 +102,11 @@ public void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration) this.bridgeConfiguration = bridgeConfiguration; } + /** + * requests all vehicles + * + * @return list of vehicles + */ public List<@NonNull Vehicle> requestVehicles() throws NetworkException { List<@NonNull Vehicle> vehicles = new ArrayList<>(); List<@NonNull VehicleBase> vehiclesBase = requestVehiclesBase(); @@ -123,12 +128,19 @@ public void setBridgeConfiguration(MyBMWBridgeConfiguration bridgeConfiguration) * request all vehicles for one specific brand and their state * * @param brand + * @return the vehicles of one brand */ public List requestVehiclesBase(String brand) throws NetworkException { String vehicleResponseString = requestVehiclesBaseJson(brand); return JsonStringDeserializer.getVehicleBaseList(vehicleResponseString); } + /** + * request the raw JSON for the vehicle + * + * @param brand + * @return the base vehicle information as JSON string + */ public String requestVehiclesBaseJson(String brand) throws NetworkException { byte[] vehicleResponse = get(vehicleUrl, brand, null, HTTPConstants.CONTENT_TYPE_JSON); String vehicleResponseString = new String(vehicleResponse, Charset.defaultCharset()); @@ -138,7 +150,7 @@ public String requestVehiclesBaseJson(String brand) throws NetworkException { /** * request vehicles for all possible brands * - * @param callback + * @return the list of vehicles */ public List requestVehiclesBase() throws NetworkException { List vehicles = new ArrayList<>(); @@ -159,9 +171,10 @@ public List requestVehiclesBase() throws NetworkException { /** * request the vehicle image * - * @param config - * @param props - * @return + * @param vin the vin of the vehicle + * @param brand the brand of the vehicle + * @param props the image properties + * @return the image as a byte array */ public byte[] requestImage(String vin, String brand, ImageProperties props) throws NetworkException { final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(bridgeConfiguration.region) @@ -172,14 +185,22 @@ public byte[] requestImage(String vin, String brand, ImageProperties props) thro /** * request the state for one specific vehicle * - * @param baseVehicle - * @return + * @param vin + * @param brand + * @return the vehicle state */ public VehicleStateContainer requestVehicleState(String vin, String brand) throws NetworkException { String vehicleStateResponseString = requestVehicleStateJson(vin, brand); return JsonStringDeserializer.getVehicleState(vehicleStateResponseString); } + /** + * request the raw state as JSON for one specific vehicle + * + * @param vin + * @param brand + * @return the vehicle state as string + */ public String requestVehicleStateJson(String vin, String brand) throws NetworkException { byte[] vehicleStateResponse = get(vehicleStateUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON); String vehicleStateResponseString = new String(vehicleStateResponse, Charset.defaultCharset()); @@ -188,13 +209,23 @@ public String requestVehicleStateJson(String vin, String brand) throws NetworkEx /** * request charge statistics for electric vehicles - * + * + * @param vin + * @param brand + * @return the charge statistics */ public ChargingStatisticsContainer requestChargeStatistics(String vin, String brand) throws NetworkException { String chargeStatisticsResponseString = requestChargeStatisticsJson(vin, brand); return JsonStringDeserializer.getChargingStatistics(new String(chargeStatisticsResponseString)); } + /** + * request charge statistics for electric vehicles as JSON + * + * @param vin + * @param brand + * @return the charge statistics as JSON string + */ public String requestChargeStatisticsJson(String vin, String brand) throws NetworkException { MultiMap<@Nullable String> chargeStatisticsParams = new MultiMap<>(); chargeStatisticsParams.put("vin", vin); @@ -210,12 +241,22 @@ public String requestChargeStatisticsJson(String vin, String brand) throws Netwo /** * request charge sessions for electric vehicles * + * @param vin + * @param brand + * @return the charge sessions */ public ChargingSessionsContainer requestChargeSessions(String vin, String brand) throws NetworkException { String chargeSessionsResponseString = requestChargeSessionsJson(vin, brand); return JsonStringDeserializer.getChargingSessions(chargeSessionsResponseString); } + /** + * request charge sessions for electric vehicles as JSON string + * + * @param vin + * @param brand + * @return the charge sessions as JSON string + */ public String requestChargeSessionsJson(String vin, String brand) throws NetworkException { MultiMap<@Nullable String> chargeSessionsParams = new MultiMap<>(); chargeSessionsParams.put("vin", vin); @@ -229,6 +270,14 @@ public String requestChargeSessionsJson(String vin, String brand) throws Network return chargeSessionsResponseString; } + /** + * execute a remote service call + * + * @param vin + * @param brand + * @param service the service which should be executed + * @return the running service execution for status checks + */ public ExecutionStatusContainer executeRemoteServiceCall(String vin, String brand, RemoteService service) throws NetworkException { String executionUrl = remoteCommandUrl + vin + "/" + service.getCommand(); @@ -238,6 +287,13 @@ public ExecutionStatusContainer executeRemoteServiceCall(String vin, String bran return JsonStringDeserializer.getExecutionStatus(new String(response)); } + /** + * check the status of a service call + * + * @param brand + * @param eventid the ID of the currently running service execution + * @return the running service execution for status checks + */ public ExecutionStatusContainer executeRemoteServiceStatusCall(String brand, String eventId) throws NetworkException { String executionUrl = remoteStatusUrl + Constants.QUESTION + "eventId=" + eventId; @@ -251,11 +307,10 @@ public ExecutionStatusContainer executeRemoteServiceStatusCall(String brand, Str * prepares a GET request to the backend * * @param url - * @param coding - * @param params * @param brand + * @param vin * @param contentType - * @return + * @return byte array of the response body */ private byte[] get(String url, final String brand, @Nullable String vin, String contentType) throws NetworkException { @@ -266,11 +321,11 @@ private byte[] get(String url, final String brand, @Nullable String vin, String * prepares a POST request to the backend * * @param url - * @param coding - * @param params * @param brand + * @param vin * @param contentType - * @return + * @param body + * @return byte array of the response body */ private byte[] post(String url, final String brand, @Nullable String vin, String contentType, @Nullable String body) throws NetworkException { @@ -281,12 +336,12 @@ private byte[] post(String url, final String brand, @Nullable String vin, String * executes the real call to the backend * * @param url - * @param post - * @param encoding - * @param queryParams + * @param post boolean value indicating if it is a post request * @param brand + * @param vin * @param contentType - * @return + * @param body + * @return byte array of the response body */ private synchronized byte[] call(final String url, final boolean post, final String brand, final @Nullable String vin, final String contentType, final @Nullable String body) throws NetworkException { From 9814602558227638993e22bce47cb63b0f2a8d36 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 22:04:56 +0100 Subject: [PATCH 57/64] [mybmw] catch InterruptedException properly Signed-off-by: Martin Grassl --- .../mybmw/internal/handler/backend/MyBMWHttpProxy.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java index 2f1570ba1fb03..debad84e2ba22 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/MyBMWHttpProxy.java @@ -387,7 +387,12 @@ private synchronized byte[] call(final String url, final boolean post, final Str ResponseContentAnonymizer.anonymizeResponseContent(body)); } } - } catch (InterruptedException | TimeoutException | ExecutionException e) { + } catch (TimeoutException | ExecutionException e) { + logResponse(ResponseContentAnonymizer.replaceVin(url, vin), e.getMessage(), + ResponseContentAnonymizer.anonymizeResponseContent(vin)); + throw new NetworkException(url, -1, null, body, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); logResponse(ResponseContentAnonymizer.replaceVin(url, vin), e.getMessage(), ResponseContentAnonymizer.anonymizeResponseContent(vin)); throw new NetworkException(url, -1, null, body, e); From 7019a00d41506cb1057873a7040b804f71e197dd Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 22:12:07 +0100 Subject: [PATCH 58/64] [mybmw] revert change of some props Signed-off-by: Martin Grassl --- .../src/main/resources/OH-INF/i18n/mybmw.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties index b2435c5eea16f..86f40ab1dbd5b 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties @@ -1,6 +1,6 @@ # Binding -binding.mybmw.name = MyBMW -binding.mybmw.description = Provides access to your Vehicle Data like MyBMW App +addon.mybmw.name = MyBMW +addon.mybmw.description = Provides access to your Vehicle Data like MyBMW App # thing types thing-type.config.mybmw.bridge.language.description = Channel data can be returned in the desired language like en, de, fr ... From 705471357e96855e7340156b0f78aa696f52dac4 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 22:16:09 +0100 Subject: [PATCH 59/64] [mybmw] revert deletion of de translations Signed-off-by: Martin Grassl --- .../resources/OH-INF/i18n/mybmw_de.properties | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties new file mode 100644 index 0000000000000..298148521b964 --- /dev/null +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties @@ -0,0 +1,244 @@ +# add-on + +addon.mybmw.name = MyBMW +addon.mybmw.description = Fahrzeugdaten über die MyBMW App + +# thing types + +thing-type.mybmw.account.label = MyBMW Benutzerkonto +thing-type.mybmw.account.description = Kontodaten für das BMW Benutzerkonto +thing-type.mybmw.bev.label = Elektrofahrzeug +thing-type.mybmw.bev.description = Batterieelektrisches Fahrzeug (bev) +thing-type.mybmw.bev_rex.label = Elektrofahrzeug mit REX +thing-type.mybmw.bev_rex.description = Elektrofahrzeug mit Range Extender (bev_rex) +thing-type.mybmw.conv.label = Konventionelles Fahrzeug +thing-type.mybmw.conv.description = Konventionelles Benzin/Diesel Fahrzeug (conv) +thing-type.mybmw.phev.label = Plug-in-Hybrid Elektrofahrzeug +thing-type.mybmw.phev.description = Konventionelles Fahrzeug mit Elektromotor (phev) + +# thing types config + +thing-type.config.mybmw.bridge.language.label = Sprachauswahl +thing-type.config.mybmw.bridge.language.description = Daten werden für die gewünschte Sprache angefordert (en, de, fr ...) +thing-type.config.mybmw.bridge.password.label = Passwort +thing-type.config.mybmw.bridge.password.description = Passwort für die MyBMW App +thing-type.config.mybmw.bridge.region.label = Region +thing-type.config.mybmw.bridge.region.description = Auswahl Ihrer Region +thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = Nordamerika +thing-type.config.mybmw.bridge.region.option.CHINA = China +thing-type.config.mybmw.bridge.region.option.ROW = Rest der Welt +thing-type.config.mybmw.bridge.userName.label = Benutzername +thing-type.config.mybmw.bridge.userName.description = Benutzername für die MyBMW App +thing-type.config.mybmw.vehicle.refreshInterval.label = Datenaktualisierung in Minuten +thing-type.config.mybmw.vehicle.refreshInterval.description = Rate der Datenaktualisierung Ihres Fahrzeugs +thing-type.config.mybmw.vehicle.vehicleBrand.label = Marke des Fahrzeugs +thing-type.config.mybmw.vehicle.vehicleBrand.description = Fahrzeugmarke wie z.B. BMW oder Mini. +thing-type.config.mybmw.vehicle.vin.label = Fahrzeug Identifikationsnummer (VIN) +thing-type.config.mybmw.vehicle.vin.description = VIN des Fahrzeugs + +# channel group types + +channel-group-type.mybmw.charge-statistic.label = Elektrische Ladestatistik +channel-group-type.mybmw.charge-statistic.description = Statistik der Ladevorgänge im Monat +channel-group-type.mybmw.check-control-values.label = Warnungen +channel-group-type.mybmw.check-control-values.description = Aktuelle Warnungen des Fahrzeugs +channel-group-type.mybmw.conv-range-values.label = Verbrenner Reichweiten und Füllstände +channel-group-type.mybmw.conv-range-values.description = Tachostand, Reichweite und Tankfüllung des Fahrzeugs +channel-group-type.mybmw.door-values.label = Details aller Türen +channel-group-type.mybmw.door-values.description = Zeigt die Details der Türen und Fenster des Fahrzeugs +channel-group-type.mybmw.ev-range-values.label = Elektro- Reichweiten und Batterieladung +channel-group-type.mybmw.ev-range-values.description = Tachostand, Reichweiten und Ladestand des Fahrzeugs +channel-group-type.mybmw.ev-vehicle-status.label = Fahrzeug Zustand +channel-group-type.mybmw.ev-vehicle-status.description = Gesamtzustand des Fahrzeugs +channel-group-type.mybmw.hybrid-range-values.label = Hybride Reichweiten und Füllstände +channel-group-type.mybmw.hybrid-range-values.description = Tachostand, Reichweite, Ladezustand und Tankfüllung für hybride Fahrzeuge +channel-group-type.mybmw.image-values.label = Fahrzeug Bild +channel-group-type.mybmw.image-values.description = Bild des Fahrzeug basierend auf der ausgewählten Ansicht +channel-group-type.mybmw.location-values.label = Fahrzeug Standort +channel-group-type.mybmw.location-values.description = Koordinaten und Ausrichtung des Fahrzeugs +channel-group-type.mybmw.profile-values.label = Elektrisches Ladeprofil +channel-group-type.mybmw.profile-values.description = Zeitplanung der Ladevorgänge +channel-group-type.mybmw.remote-services.label = Fernsteuerung +channel-group-type.mybmw.remote-services.description = Fernsteuerung des Fahrzeugs +channel-group-type.mybmw.service-values.label = Wartung +channel-group-type.mybmw.service-values.description = Anstehende Wartungstermine des Fahrzeugs +channel-group-type.mybmw.session-values.label = Elektrische Ladevorgänge +channel-group-type.mybmw.session-values.description = Liste der letzten Ladevorgänge +channel-group-type.mybmw.tire-pressures.label = Reifen Luftdruck +channel-group-type.mybmw.tire-pressures.description = Reifen Luftdruck Ist und Sollwerte +channel-group-type.mybmw.vehicle-status.label = Fahrzeug Zustand +channel-group-type.mybmw.vehicle-status.description = Gesamtzustand des Fahrzeugs + +# channel types + +channel-type.mybmw.address-channel.label = Adresse +channel-type.mybmw.charging-info-channel.label = Ladeinformationen +channel-type.mybmw.charging-status-channel.label = Ladezustand +channel-type.mybmw.check-control-channel.label = Warnung Aktiv +channel-type.mybmw.checkcontrol-details-channel.label = Warnung Details +channel-type.mybmw.checkcontrol-name-channel.label = Warnung +channel-type.mybmw.checkcontrol-severity-channel.label = Warnung Priorität +channel-type.mybmw.doors-channel.label = Gesamtzustand der Türen +channel-type.mybmw.driver-front-channel.label = Fahrertür +channel-type.mybmw.driver-rear-channel.label = Fahrertür Hinten +channel-type.mybmw.front-left-current-channel.label = Reifen Luftdruck Vorne Links +channel-type.mybmw.front-left-target-channel.label = Reifen Luftdruck vorne links +channel-type.mybmw.front-right-current-channel.label = Reifen Luftdruck Vorne Rechts +channel-type.mybmw.front-right-target-channel.label = Reifen Luftdruck vorne rechts +channel-type.mybmw.gps-channel.label = Koordinaten +channel-type.mybmw.heading-channel.label = Ausrichtung +channel-type.mybmw.home-distance-channel.label = Entfernung von Zuhause +channel-type.mybmw.home-distance-channel.description = Berechnete Entfernung zwischen Fahrzeug und Heimatort +channel-type.mybmw.hood-channel.label = Frontklappe +channel-type.mybmw.image-view-channel.label = Fahrzeug Ansicht +channel-type.mybmw.image-view-channel.command.option.VehicleStatus = Front Seitenansicht +channel-type.mybmw.image-view-channel.command.option.VehicleInfo = Frontansicht +channel-type.mybmw.image-view-channel.command.option.ChargingHistory = Seitenansicht +channel-type.mybmw.image-view-channel.command.option.Default = Standard Ansicht +channel-type.mybmw.last-update-channel.label = Letzte Aktualisierung +channel-type.mybmw.last-update-channel.state.pattern = %1$tA, %1$td.%1$tm. %1$tH\:%1$tM +channel-type.mybmw.lock-channel.label = Fahrzeug Abgeschlossen +channel-type.mybmw.mileage-channel.label = Tachostand +channel-type.mybmw.motion-channel.label = Fahrzustand +channel-type.mybmw.next-service-date-channel.label = Nächster Service Termin +channel-type.mybmw.next-service-date-channel.state.pattern = %1$tb %1$tY +channel-type.mybmw.next-service-mileage-channel.label = Nächster Service in Kilometern +channel-type.mybmw.passenger-front-channel.label = Beifahrertür +channel-type.mybmw.passenger-rear-channel.label = Beifahrertür Hinten +channel-type.mybmw.plug-connection-channel.label = Ladestecker +channel-type.mybmw.png-channel.label = Fahrzeug Bild +channel-type.mybmw.profile-climate-channel.label = Klimatisierung bei Abfahrt +channel-type.mybmw.profile-control-channel.label = Ladeplan +channel-type.mybmw.profile-control-channel.description = Ladeplan Auswahl +channel-type.mybmw.profile-control-channel.command.option.weeklyPlanner = Wochenplan +channel-type.mybmw.profile-limit-channel.label = Ladung Limitiert +channel-type.mybmw.profile-limit-channel.description = Limitiertes Laden aktiviert +channel-type.mybmw.profile-mode-channel.label = Ladeprofil +channel-type.mybmw.profile-mode-channel.description = Modus für sofortiges oder verzögertes Laden +channel-type.mybmw.profile-mode-channel.command.option.immediateCharging = Sofort Laden +channel-type.mybmw.profile-mode-channel.command.option.delayedCharging = Ladeverzögerung +channel-type.mybmw.profile-prefs-channel.label = Ladeprofil Präferenz +channel-type.mybmw.profile-prefs-channel.description = Einstellungen für verzögerte Ladung +channel-type.mybmw.profile-prefs-channel.command.option.noPreSelection = Keine Präferenz +channel-type.mybmw.profile-prefs-channel.command.option.chargingWindow = Laden im Zeitfenster +channel-type.mybmw.profile-target-channel.label = Ziel Ladezustand +channel-type.mybmw.profile-target-channel.description = Erwünschter Batterie Ladezustand +channel-type.mybmw.range-electric-channel.label = Elektrische Reichweite +channel-type.mybmw.range-fuel-channel.label = Verbrenner Reichweite +channel-type.mybmw.range-hybrid-channel.label = Hybride Reichweite +channel-type.mybmw.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius +channel-type.mybmw.range-radius-fuel-channel.label = Verbrenner Reichweiten-Radius +channel-type.mybmw.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius +channel-type.mybmw.raw-channel.label = Rohdaten +channel-type.mybmw.rear-left-current-channel.label = Reifen Luftdruck Hinten Links +channel-type.mybmw.rear-left-target-channel.label = Reifen Luftdruck hinten links +channel-type.mybmw.rear-right-current-channel.label = Reifen Luftdruck Hinten Rechts +channel-type.mybmw.rear-right-target-channel.label = Reifen Luftdruck hinten rechts +channel-type.mybmw.remaining-fuel-channel.label = Tankstand +channel-type.mybmw.remote-command-channel.label = Kommando Auswahl +channel-type.mybmw.remote-state-channel.label = Ausführungszustand +channel-type.mybmw.service-date-channel.label = Service Termin +channel-type.mybmw.service-date-channel.state.pattern = %1$tb %1$tY +channel-type.mybmw.service-details-channel.label = Service Details +channel-type.mybmw.service-mileage-channel.label = Service in Kilometern +channel-type.mybmw.service-name-channel.label = Service +channel-type.mybmw.session-energy-channel.label = Energie Geladen +channel-type.mybmw.session-issue-channel.label = Ladevorgang Probleme +channel-type.mybmw.session-status-channel.label = Ladevorgang Zustand +channel-type.mybmw.session-subtitle-channel.label = Ladevorgang Details +channel-type.mybmw.session-title-channel.label = Ladevorgang Beschreibung +channel-type.mybmw.soc-channel.label = Batterie Ladestand +channel-type.mybmw.statistic-energy-channel.label = Energie Geladen Monat +channel-type.mybmw.statistic-energy-channel.description = Geladene Energie in diesem Monat +channel-type.mybmw.statistic-sessions-channel.label = Ladevorgänge Monat +channel-type.mybmw.statistic-sessions-channel.description = Anzahl der Ladevorgänge in diesem Monat +channel-type.mybmw.statistic-title-channel.label = Ladestatistik Monat +channel-type.mybmw.sunroof-channel.label = Schiebedach +channel-type.mybmw.timer1-day-fri-channel.label = Zeitprofil 1 - Freitag +channel-type.mybmw.timer1-day-fri-channel.description = Freitags Planung für Timer 1 +channel-type.mybmw.timer1-day-mon-channel.label = Zeitprofil 1 - Montag +channel-type.mybmw.timer1-day-mon-channel.description = Montags Planung für Timer 1 +channel-type.mybmw.timer1-day-sat-channel.label = Zeitprofil 1 - Samstag +channel-type.mybmw.timer1-day-sat-channel.description = Samstags Planung für Timer 1 +channel-type.mybmw.timer1-day-sun-channel.label = Zeitprofil 1 - Sonntag +channel-type.mybmw.timer1-day-sun-channel.description = Sonntags Planung für Timer 1 +channel-type.mybmw.timer1-day-thu-channel.label = Zeitprofil 1 - Donnerstag +channel-type.mybmw.timer1-day-thu-channel.description = Donnerstags Planung für Timer 1 +channel-type.mybmw.timer1-day-tue-channel.label = Zeitprofil 1 - Dienstag +channel-type.mybmw.timer1-day-tue-channel.description = Dienstags Planung für Timer 1 +channel-type.mybmw.timer1-day-wed-channel.label = Zeitprofil 1 - Mittwoch +channel-type.mybmw.timer1-day-wed-channel.description = Mittwochs Planung für Timer 1 +channel-type.mybmw.timer1-departure-channel.label = Zeitprofil 1 - Abfahrtszeit +channel-type.mybmw.timer1-departure-channel.description = Abfahrtszeit für regelmäßige Planung Timer 1 +channel-type.mybmw.timer1-departure-channel.state.pattern = %1$tH\:%1$tM +channel-type.mybmw.timer1-enabled-channel.label = Zeitprofil 1 - Aktiviert +channel-type.mybmw.timer1-enabled-channel.description = Timer 1 aktiviert +channel-type.mybmw.timer2-day-fri-channel.label = Zeitprofil 2 - Freitag +channel-type.mybmw.timer2-day-fri-channel.description = Freitags Planung für Timer 2 +channel-type.mybmw.timer2-day-mon-channel.label = Zeitprofil 2 - Montag +channel-type.mybmw.timer2-day-mon-channel.description = Montags Planung für Timer 2 +channel-type.mybmw.timer2-day-sat-channel.label = Zeitprofil 2 - Samstag +channel-type.mybmw.timer2-day-sat-channel.description = Samstags Planung für Timer 2 +channel-type.mybmw.timer2-day-sun-channel.label = Zeitprofil 2 - Sonntag +channel-type.mybmw.timer2-day-sun-channel.description = Sonntags Planung für Timer 2 +channel-type.mybmw.timer2-day-thu-channel.label = Zeitprofil 2 - Donnerstag +channel-type.mybmw.timer2-day-thu-channel.description = Donnerstags Planung für Timer 2 +channel-type.mybmw.timer2-day-tue-channel.label = Zeitprofil 2 - Dienstag +channel-type.mybmw.timer2-day-tue-channel.description = Dienstags Planung für Timer 2 +channel-type.mybmw.timer2-day-wed-channel.label = Zeitprofil 2 - Mittwoch +channel-type.mybmw.timer2-day-wed-channel.description = Mittwochs Planung für Timer 2 +channel-type.mybmw.timer2-departure-channel.label = Zeitprofil 2 - Abfahrtszeit +channel-type.mybmw.timer2-departure-channel.description = Abfahrtszeit für regelmäßige Planung Timer 2 +channel-type.mybmw.timer2-departure-channel.state.pattern = %1$tH\:%1$tM +channel-type.mybmw.timer2-enabled-channel.label = Zeitprofil 2 - Aktiviert +channel-type.mybmw.timer2-enabled-channel.description = Timer 2 aktiviert +channel-type.mybmw.timer3-day-fri-channel.label = Zeitprofil 3 - Freitag +channel-type.mybmw.timer3-day-fri-channel.description = Freitags Planung für Timer 3 +channel-type.mybmw.timer3-day-mon-channel.label = Zeitprofil 3 - Montag +channel-type.mybmw.timer3-day-mon-channel.description = Montags Planung für Timer 3 +channel-type.mybmw.timer3-day-sat-channel.label = Zeitprofil 3 - Samstag +channel-type.mybmw.timer3-day-sat-channel.description = Samstags Planung für Timer 3 +channel-type.mybmw.timer3-day-sun-channel.label = Zeitprofil 3 - Sonntag +channel-type.mybmw.timer3-day-sun-channel.description = Sonntags Planung für Timer 3 +channel-type.mybmw.timer3-day-thu-channel.label = Zeitprofil 3 - Donnerstag +channel-type.mybmw.timer3-day-thu-channel.description = Donnerstags Planung für Timer 3 +channel-type.mybmw.timer3-day-tue-channel.label = Zeitprofil 3 - Dienstag +channel-type.mybmw.timer3-day-tue-channel.description = Dienstags Planung für Timer 3 +channel-type.mybmw.timer3-day-wed-channel.label = Zeitprofil 3 - Mittwoch +channel-type.mybmw.timer3-day-wed-channel.description = Mittwochs Planung für Timer 3 +channel-type.mybmw.timer3-departure-channel.label = Zeitprofil 3 - Abfahrtszeit +channel-type.mybmw.timer3-departure-channel.description = Abfahrtszeit für regelmäßige Planung Timer 3 +channel-type.mybmw.timer3-departure-channel.state.pattern = %1$tH\:%1$tM +channel-type.mybmw.timer3-enabled-channel.label = Zeitprofil 3 - Aktiviert +channel-type.mybmw.timer3-enabled-channel.description = Timer 3 aktiviert +channel-type.mybmw.timer4-day-fri-channel.label = Zeitprofil 4 - Freitag +channel-type.mybmw.timer4-day-fri-channel.description = Freitags Planung für Timer 4 +channel-type.mybmw.timer4-day-mon-channel.label = Zeitprofil 4 - Montag +channel-type.mybmw.timer4-day-mon-channel.description = Montags Planung für Timer 4 +channel-type.mybmw.timer4-day-sat-channel.label = Zeitprofil 4 - Samstag +channel-type.mybmw.timer4-day-sat-channel.description = Samstags Planung für Timer 4 +channel-type.mybmw.timer4-day-sun-channel.label = Zeitprofil 4 - Sonntag +channel-type.mybmw.timer4-day-sun-channel.description = Sonntags Planung für Timer 4 +channel-type.mybmw.timer4-day-thu-channel.label = Zeitprofil 4 - Donnerstag +channel-type.mybmw.timer4-day-thu-channel.description = Donnerstags Planung für Timer 4 +channel-type.mybmw.timer4-day-tue-channel.label = Zeitprofil 4 - Dienstag +channel-type.mybmw.timer4-day-tue-channel.description = Dienstags Planung für Timer 4 +channel-type.mybmw.timer4-day-wed-channel.label = Zeitprofil 4 - Mittwoch +channel-type.mybmw.timer4-day-wed-channel.description = Mittwochs Planung für Timer 4 +channel-type.mybmw.timer4-departure-channel.label = Zeitprofil 4 - Abfahrtszeit +channel-type.mybmw.timer4-departure-channel.description = Abfahrtszeit für regelmäßige Planung Timer 4 +channel-type.mybmw.timer4-departure-channel.state.pattern = %1$tH\:%1$tM +channel-type.mybmw.timer4-enabled-channel.label = Zeitprofil 4 - Aktiviert +channel-type.mybmw.timer4-enabled-channel.description = Timer 4 aktiviert +channel-type.mybmw.trunk-channel.label = Heckklappe +channel-type.mybmw.window-driver-front-channel.label = Fahrertür Fenster +channel-type.mybmw.window-driver-rear-channel.label = Fahrertür Hinten Fenster +channel-type.mybmw.window-end-channel.label = Ladefenster Endzeit +channel-type.mybmw.window-end-channel.description = Endzeit des Ladefensters +channel-type.mybmw.window-end-channel.state.pattern = %1$tH\:%1$tM +channel-type.mybmw.window-passenger-front-channel.label = Beifahrertür Fenster +channel-type.mybmw.window-passenger-rear-channel.label = Beifahrertür Hinten Fenster +channel-type.mybmw.window-start-channel.label = Ladefenster Startzeit +channel-type.mybmw.window-start-channel.description = Startzeit des Ladefensters +channel-type.mybmw.window-start-channel.state.pattern = %1$tH\:%1$tM +channel-type.mybmw.windows-channel.label = Gesamtzustand der Fenster \ No newline at end of file From 1b971b174a0c280630a011ceeb40f2fa890e3ba4 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Wed, 13 Dec 2023 22:26:22 +0100 Subject: [PATCH 60/64] [mybmw] fix missing line break at end of file Signed-off-by: Martin Grassl --- .../src/main/resources/OH-INF/i18n/mybmw_de.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties index 298148521b964..bffc3a83bf421 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw_de.properties @@ -241,4 +241,4 @@ channel-type.mybmw.window-passenger-rear-channel.label = Beifahrertür Hinten Fe channel-type.mybmw.window-start-channel.label = Ladefenster Startzeit channel-type.mybmw.window-start-channel.description = Startzeit des Ladefensters channel-type.mybmw.window-start-channel.state.pattern = %1$tH\:%1$tM -channel-type.mybmw.windows-channel.label = Gesamtzustand der Fenster \ No newline at end of file +channel-type.mybmw.windows-channel.label = Gesamtzustand der Fenster From 74f26a40d4ec9d16f352f9aca337428a8eef2af8 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Wed, 13 Dec 2023 23:39:08 +0100 Subject: [PATCH 61/64] Update bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties Signed-off-by: Jacob Laursen --- .../src/main/resources/OH-INF/i18n/mybmw.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties index 86f40ab1dbd5b..efe4407fc345c 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/i18n/mybmw.properties @@ -1,4 +1,5 @@ -# Binding +# add-on + addon.mybmw.name = MyBMW addon.mybmw.description = Provides access to your Vehicle Data like MyBMW App From 54b3116900048a4fd37b580dda466be835a6f32c Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Thu, 14 Dec 2023 08:36:12 +0100 Subject: [PATCH 62/64] [mybmw] decrease log level of JSON deserialization error Signed-off-by: Martin Grassl --- .../handler/backend/JsonStringDeserializer.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java index 8ee1f7dda8597..f9addb18ae506 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java +++ b/bundles/org.openhab.binding.mybmw/src/main/java/org/openhab/binding/mybmw/internal/handler/backend/JsonStringDeserializer.java @@ -46,7 +46,7 @@ public static List getVehicleBaseList(String vehicleBaseJson) { VehicleBase[] vehicleBaseArray = deserializeString(vehicleBaseJson, VehicleBase[].class); return Arrays.asList(vehicleBaseArray); } catch (JsonSyntaxException e) { - LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + LOGGER.debug("JsonSyntaxException {}", e.getMessage()); return new ArrayList(); } } @@ -57,7 +57,7 @@ public static VehicleStateContainer getVehicleState(String vehicleStateJson) { vehicleState.setRawStateJson(vehicleStateJson); return vehicleState; } catch (JsonSyntaxException e) { - LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + LOGGER.debug("JsonSyntaxException {}", e.getMessage()); return new VehicleStateContainer(); } } @@ -68,7 +68,7 @@ public static ChargingStatisticsContainer getChargingStatistics(String chargeSta ChargingStatisticsContainer.class); return chargeStatistics; } catch (JsonSyntaxException e) { - LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + LOGGER.debug("JsonSyntaxException {}", e.getMessage()); return new ChargingStatisticsContainer(); } } @@ -77,7 +77,7 @@ public static ChargingSessionsContainer getChargingSessions(String chargeSession try { return deserializeString(chargeSessionsJson, ChargingSessionsContainer.class); } catch (JsonSyntaxException e) { - LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + LOGGER.debug("JsonSyntaxException {}", e.getMessage()); return new ChargingSessionsContainer(); } } @@ -86,7 +86,7 @@ public static ExecutionStatusContainer getExecutionStatus(String executionStatus try { return deserializeString(executionStatusJson, ExecutionStatusContainer.class); } catch (JsonSyntaxException e) { - LOGGER.warn("JsonSyntaxException {}", e.getMessage()); + LOGGER.debug("JsonSyntaxException {}", e.getMessage()); return new ExecutionStatusContainer(); } } From 7e4b42c05562e1cb1fd7966a613f1f99e129e1f8 Mon Sep 17 00:00:00 2001 From: Martin Grassl Date: Thu, 14 Dec 2023 08:40:21 +0100 Subject: [PATCH 63/64] [mybmw] remove dimension of some numbers Signed-off-by: Martin Grassl --- .../src/main/resources/OH-INF/thing/range-channel-types.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml index 81adde871f35a..946700e4f2c33 100644 --- a/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml +++ b/bundles/org.openhab.binding.mybmw/src/main/resources/OH-INF/thing/range-channel-types.xml @@ -34,12 +34,12 @@ - Number:Dimensionless + Number - Number:Dimensionless + Number From 4588674c6ec12d10969d8cbd16d74756e639d0d9 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Thu, 14 Dec 2023 14:11:15 +0100 Subject: [PATCH 64/64] Update bundles/org.openhab.binding.mybmw/README.md Signed-off-by: Jacob Laursen --- bundles/org.openhab.binding.mybmw/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mybmw/README.md b/bundles/org.openhab.binding.mybmw/README.md index b8ba468f546da..5fc2360847601 100644 --- a/bundles/org.openhab.binding.mybmw/README.md +++ b/bundles/org.openhab.binding.mybmw/README.md @@ -247,8 +247,8 @@ See description [Range vs Range Radius](#range-vs-range-radius) to get more info | 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 | | -| Estimated Fuel Consumption l/100km | estimated-fuel-l-100km | Number:Dimensionless | X | X | X | | -| Estimated Fuel Consumption mpg | estimated-fuel-mpg | Number:Dimensionless | X | X | X | | +| Estimated Fuel Consumption l/100km | estimated-fuel-l-100km | Number | X | X | X | | +| Estimated Fuel Consumption mpg | estimated-fuel-mpg | Number | 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 | |