[mybmw] Upgrade to new BMW API (#14452)

* [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

Fixes #14065

Also-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Martin Grassl <martin.grassl@digital-filestore.de>
This commit is contained in:
Martin Grassl 2023-12-14 23:08:25 +01:00 committed by GitHub
parent 6d2b8bc92f
commit 4f84c48b21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
223 changed files with 12430 additions and 14679 deletions

View File

@ -225,7 +225,7 @@
/bundles/org.openhab.binding.mqtt.homie/ @ccutrer
/bundles/org.openhab.binding.mqtt.ruuvigateway/ @ssalonen
/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.mynice/ @clinique
/bundles/org.openhab.binding.mystrom/ @pail23
/bundles/org.openhab.binding.nanoleaf/ @stefan-hoehn

View File

@ -163,9 +163,9 @@ 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 |
| 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 |
Overall Door Status values
@ -239,17 +239,19 @@ See description [Range vs Range Radius](#range-vs-range-radius) to get more info
- 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 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 | 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 | |
#### Doors Details
@ -359,6 +361,7 @@ The channel _command_ provides options
- _horn-blow_
- _climate-now-start_
- _climate-now-stop_
- _charge-now_
The channel _state_ shows the progress of the command execution in the following order
@ -471,10 +474,11 @@ 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
@ -491,7 +495,8 @@ There are 3 occurrences of dynamic data delivered
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.
Select the desired service and the corresponding Charge Session with _Energy Charged_, _Session Status_ and
_Session Issues_ will be shown.
### TroubleShooting
@ -507,32 +512,34 @@ If these preconditions are fulfilled proceed with the fingerprint generation.
#### Generate Debug Fingerprint
<img align="right" src="./doc/DiscoveryScan.png" width="400" height="350"/>
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 copiedfrom there.
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.
```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
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.
The package.subpackage in this case would be "org.openhab.binding.mybmw".
As with fingerprint data, personal data is eliminated from logs.
### Range vs Range Radius
<img align="right" src="./doc/range-radius.png" width="400" height="350"/>

View File

@ -14,4 +14,171 @@
<name>openHAB Add-ons :: Bundles :: MyBMW Binding</name>
<profiles>
<profile>
<id>test-coverage</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<jacoco-agent.destfile>target/jacoco.exec</jacoco-agent.destfile>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
<executions>
<execution>
<id>default-instrument</id>
<goals>
<goal>instrument</goal>
</goals>
</execution>
<execution>
<id>default-restore-instrumented-classes</id>
<phase>test</phase>
<goals>
<goal>restore-instrumented-classes</goal>
</goals>
</execution>
<execution>
<id>default-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>default-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.20</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.20</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<!-- must be on the classpath -->
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.agent</artifactId>
<classifier>runtime</classifier>
<version>0.8.8</version>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
<profile>
<!--
If you activate this profile, the MyBmwProxyIT is executed which means real
backend requests. The test is only successful if you provide CONNECTED_USER and
CONNECTED_PASSWORD as environment variable of the Maven command.
-->
<id>integration-tests</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0-M7</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<!--
This profile generates a jar file <regular-jar-file-name>-testenv.jar in the target folder. This
testenv jar contains the regular classes and in addition all responses from
src/test/resources. If you copy this jar file to your addons folder, you can simulate all
accounts which are available as fingerprints in the responses folder. This can be done like
this:
1. start openhab with the environment variable "ENVIRONMENT=test"
2. configure the connected account with username "testuser"
3. configure as connected password the folder which you want to test, e.g. "BEV", "BEV2", "PHEV", "ICE", "ICE2",
"MILD_HYBRID"
after that you should get the vehicles loaded properly so you can check if the channels are populated with data properly.
-->
<id>test-jar</id>
<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>copy-resources</id>
<!-- here the phase you need -->
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/classes</outputDirectory>
<resources>
<resource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classifier>testenv</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -16,12 +16,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
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

View File

@ -23,20 +23,46 @@ import org.openhab.core.thing.ThingTypeUID;
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit and 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 String REFRESH_INTERVAL = "refreshInterval";
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;
// 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 +82,150 @@ public class MyBMWConstants {
}
}
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<String> FUEL_VEHICLES = Set.of(VehicleType.CONVENTIONAL.toString(),
static final Set<String> FUEL_VEHICLES = Set.of(VehicleType.CONVENTIONAL.toString(),
VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
public static final Set<String> ELECTRIC_VEHICLES = Set.of(VehicleType.ELECTRIC.toString(),
static final Set<String> ELECTRIC_VEHICLES = Set.of(VehicleType.ELECTRIC.toString(),
VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CONNECTED_DRIVE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_CONV = new ThingTypeUID(BINDING_ID,
VehicleType.CONVENTIONAL.toString());
public static final ThingTypeUID THING_TYPE_PHEV = new ThingTypeUID(BINDING_ID,
VehicleType.PLUGIN_HYBRID.toString());
public static final ThingTypeUID THING_TYPE_BEV_REX = new ThingTypeUID(BINDING_ID,
VehicleType.ELECTRIC_REX.toString());
public static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC.toString());
public static final Set<ThingTypeUID> SUPPORTED_THING_SET = Set.of(THING_TYPE_CONNECTED_DRIVE_ACCOUNT,
THING_TYPE_CONV, THING_TYPE_PHEV, THING_TYPE_BEV_REX, THING_TYPE_BEV);
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<ThingTypeUID> SUPPORTED_THING_SET = Set.of(THING_TYPE_CONNECTED_DRIVE_ACCOUNT, THING_TYPE_CONV,
THING_TYPE_PHEV, THING_TYPE_BEV_REX, THING_TYPE_BEV);
// Thing Group definitions
public static final String CHANNEL_GROUP_STATUS = "status";
public static final String CHANNEL_GROUP_SERVICE = "service";
public static final String CHANNEL_GROUP_CHECK_CONTROL = "check";
public static final String CHANNEL_GROUP_DOORS = "doors";
public static final String CHANNEL_GROUP_RANGE = "range";
public static final String CHANNEL_GROUP_LOCATION = "location";
public static final String CHANNEL_GROUP_REMOTE = "remote";
public static final String CHANNEL_GROUP_CHARGE_PROFILE = "profile";
public static final String CHANNEL_GROUP_CHARGE_STATISTICS = "statistic";
public static final String CHANNEL_GROUP_CHARGE_SESSION = "session";
public static final String CHANNEL_GROUP_TIRES = "tires";
public static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image";
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";
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";
public static final String REMOTE_SERVICE_COMMAND = "command";
public static final String REMOTE_STATE = "state";
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";
}

View File

@ -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.MyBMWCommandOptionProvider;
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;
@ -37,6 +39,7 @@ import org.osgi.service.component.annotations.Reference;
* handlers.
*
* @author Bernd Weymann - Initial contribution
* @author Martin Grassl - changed localeProvider handling
*/
@NonNullByDefault
@Component(configurationPid = "binding.mybmw", service = ThingHandlerFactory.class)
@ -44,15 +47,19 @@ public class MyBMWHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory;
private final MyBMWCommandOptionProvider commandOptionProvider;
private final LocationProvider locationProvider;
private String localeLanguage;
private final TimeZoneProvider timeZoneProvider;
private final LocaleProvider localeProvider;
@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.localeProvider = localeProvider;
}
@Override
@ -64,9 +71,10 @@ public class MyBMWHandlerFactory extends BaseThingHandlerFactory {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(thingTypeUID)) {
return new MyBMWBridgeHandler((Bridge) thing, httpClientFactory, localeLanguage);
return new MyBMWBridgeHandler((Bridge) thing, httpClientFactory, localeProvider);
} 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;
}

View File

@ -0,0 +1,97 @@
/**
* 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 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 + "]";
}
}

View File

@ -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;
}

View File

@ -0,0 +1,327 @@
/**
* 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.console;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.BINDING_ID;
import static org.openhab.binding.mybmw.internal.MyBMWConstants.THING_TYPE_CONNECTED_DRIVE_ACCOUNT;
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
* @author Martin Grassl - improved exception handling
*/
@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<MyBMWBridgeHandler> 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<MyBMWBridgeHandler> 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<VehicleBase> 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: " + e.getMessage());
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: " + e.getMessage());
}
}
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<String> getUsages() {
return Arrays.asList(
new String[] { buildCommandUsage(FINGERPRINT, "generate fingerprint for all vehicles on all accounts"),
buildCommandUsage(FINGERPRINT + " <userName>", "generate fingerprint for vehicles on account"),
buildCommandUsage(FINGERPRINT + " <userName> <vin>",
"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<String> 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<VehicleBase> 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;
}
}

View File

@ -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.Logger;
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";
public class VehicleDiscovery extends AbstractDiscoveryService implements ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(VehicleDiscovery.class);
private static final int DISCOVERY_TIMEOUT = 10;
private Optional<MyBMWBridgeHandler> bridgeHandler = Optional.empty();
private Optional<MyBMWProxy> myBMWProxy = Optional.empty();
private Optional<ThingUID> bridgeUid = Optional.empty();
public VehicleDiscovery() {
super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
}
public void onResponse(List<Vehicle> vehicleList) {
bridgeHandler.ifPresent(bridge -> {
final ThingUID bridgeUID = bridge.getThing().getUID();
vehicleList.forEach(vehicle -> {
// the DriveTrain field in the delivered json is defining the Vehicle Type
String vehicleType = VehicleStatusUtils.vehicleType(vehicle.driveTrain, vehicle.model).toString();
SUPPORTED_THING_SET.forEach(entry -> {
if (entry.getId().equals(vehicleType)) {
ThingUID uid = new ThingUID(entry, vehicle.vin, bridgeUID.getId());
Map<String, String> properties = new HashMap<>();
// Vehicle Properties
properties.put("vehicleModel", vehicle.model);
properties.put("vehicleDriveTrain", vehicle.driveTrain);
properties.put("vehicleConstructionYear", Integer.toString(vehicle.year));
properties.put("vehicleBodytype", vehicle.bodyType);
properties.put("servicesSupported", getServices(vehicle, SUPPORTED_SUFFIX, true));
properties.put("servicesUnsupported", getServices(vehicle, SUPPORTED_SUFFIX, false));
String servicesEnabled = getServices(vehicle, ENABLED_SUFFIX, true) + Constants.SEMICOLON
+ getServices(vehicle, ENABLE_SUFFIX, true);
properties.put("servicesEnabled", servicesEnabled.trim());
String servicesDisabled = getServices(vehicle, ENABLED_SUFFIX, false) + Constants.SEMICOLON
+ getServices(vehicle, ENABLE_SUFFIX, false);
properties.put("servicesDisabled", servicesDisabled.trim());
// For RemoteServices we need to do it step-by-step
StringBuffer remoteServicesEnabled = new StringBuffer();
StringBuffer remoteServicesDisabled = new StringBuffer();
if (vehicle.capabilities.lock.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.unlock.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.lights.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.horn.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.vehicleFinder.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.climateNow.isEnabled) {
remoteServicesEnabled.append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
+ Constants.SEMICOLON);
} else {
remoteServicesDisabled
.append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
+ Constants.SEMICOLON);
}
properties.put("remoteServicesEnabled", remoteServicesEnabled.toString().trim());
properties.put("remoteServicesDisabled", remoteServicesDisabled.toString().trim());
// Update Properties for already created Things
bridge.getThing().getThings().forEach(vehicleThing -> {
Configuration c = vehicleThing.getConfiguration();
if (c.containsKey(MyBMWConstants.VIN)) {
String thingVIN = c.get(MyBMWConstants.VIN).toString();
if (vehicle.vin.equals(thingVIN)) {
vehicleThing.setProperties(properties);
}
}
});
// Properties needed for functional Thing
properties.put(MyBMWConstants.VIN, vehicle.vin);
properties.put("vehicleBrand", vehicle.brand);
properties.put("refreshInterval",
Integer.toString(MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES));
String vehicleLabel = vehicle.brand + " " + vehicle.model;
Map<String, Object> convertedProperties = new HashMap<String, Object>(properties);
thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
.withRepresentationProperty(MyBMWConstants.VIN).withLabel(vehicleLabel)
.withProperties(convertedProperties).build());
}
});
});
});
super(MyBMWConstants.SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof MyBMWBridgeHandler bmwBridgeHandler) {
logger.trace("VehicleDiscovery.setThingHandler for MybmwBridge");
bridgeHandler = Optional.of(bmwBridgeHandler);
bridgeHandler.get().setDiscoveryService(this);
bridgeHandler.get().setVehicleDiscovery(this);
bridgeUid = Optional.of(bridgeHandler.get().getThing().getUID());
}
}
@ -174,50 +79,154 @@ public class VehicleDiscovery extends AbstractDiscoveryService implements Discov
@Override
protected void startScan() {
bridgeHandler.ifPresent(MyBMWBridgeHandler::requestVehicles);
logger.trace("VehicleDiscovery.startScan");
discoverVehicles();
}
@Override
public void deactivate() {
logger.trace("VehicleDiscovery.deactivate");
super.deactivate();
}
public static String getServices(Vehicle vehicle, String suffix, boolean enabled) {
StringBuffer sb = new StringBuffer();
List<String> l = getObject(vehicle.capabilities, enabled);
for (String capEntry : l) {
// remove "is" prefix
String cut = capEntry.substring(2);
if (cut.endsWith(suffix)) {
if (sb.length() > 0) {
sb.append(Constants.SEMICOLON);
public void discoverVehicles() {
logger.trace("VehicleDiscovery.discoverVehicles");
myBMWProxy = bridgeHandler.get().getMyBmwProxy();
try {
Optional<List<@NonNull Vehicle>> 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
*
* 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
*/
public static List<String> getObject(Object dto, Object compare) {
List<String> l = new ArrayList<String>();
for (Field field : dto.getClass().getDeclaredFields()) {
try {
Object value = field.get(dto);
if (compare.equals(value)) {
l.add(field.getName());
private void processVehicles(List<Vehicle> vehicleList) {
logger.trace("VehicleDiscovery.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<String, String> properties = generateProperties(vehicle);
boolean thingFound = false;
// Update Properties for already created Things
List<Thing> vehicleThings = bridgeHandler.get().getThing().getThings();
for (Thing vehicleThing : vehicleThings) {
Configuration configuration = vehicleThing.getConfiguration();
if (configuration.containsKey(MyBMWConstants.VIN)) {
String thingVIN = configuration.get(MyBMWConstants.VIN).toString();
if (vehicle.getVehicleBase().getVin().equals(thingVIN)) {
vehicleThing.setProperties(properties);
thingFound = true;
}
}
}
// 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();
Map<String, Object> convertedProperties = new HashMap<String, Object>(properties);
convertedProperties.put(MyBMWConstants.VIN, vehicle.getVehicleBase().getVin());
convertedProperties.put(MyBMWConstants.VEHICLE_BRAND, vehicleAttributes.getBrand());
convertedProperties.put(MyBMWConstants.REFRESH_INTERVAL,
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<String, String> generateProperties(Vehicle vehicle) {
Map<String, String> properties = new HashMap<>();
// Vehicle Properties
VehicleAttributes vehicleAttributes = vehicle.getVehicleBase().getAttributes();
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(MyBMWConstants.SERVICES_SUPPORTED,
vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.SUPPORTED_SUFFIX, true));
properties.put(MyBMWConstants.SERVICES_UNSUPPORTED,
vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.SUPPORTED_SUFFIX, false));
properties.put(MyBMWConstants.SERVICES_ENABLED,
vehicleCapabilities.getCapabilitiesAsString(VehicleCapabilities.ENABLED_SUFFIX, true));
properties.put(MyBMWConstants.SERVICES_DISABLED,
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(MyBMWConstants.REMOTE_SERVICES_ENABLED, remoteServicesEnabled.toString().trim());
properties.put(MyBMWConstants.REMOTE_SERVICES_DISABLED, remoteServicesDisabled.toString().trim());
return properties;
}
}

View File

@ -18,6 +18,7 @@ import java.util.List;
* 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<String> 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 + "]";
}
}

View File

@ -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 and send of charge profile
*/
public class ChargeProfile {
public static final Timer INVALID_TIMER = new Timer();
public ChargingWindow reductionOfChargeCurrent;
public String chargingMode;// ": "immediateCharging",
public String chargingPreference;// ": "chargingWindow",
public String chargingControlType;// ": "weeklyPlanner",
public List<Timer> departureTimes;
public boolean climatisationOn;// ": false,
public ChargingSettings chargingSettings;
public Timer getTimerId(int id) {
if (departureTimes != null) {
for (Timer t : departureTimes) {
if (t.id == id) {
return t;
}
}
}
return INVALID_TIMER;
}
}

View File

@ -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
}

View File

@ -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<ChargeSession> sessions;
}

View File

@ -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"
}

View File

@ -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;// ": {
}

View File

@ -0,0 +1,80 @@
/**
* 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.ArrayList;
import java.util.List;
/**
* The {@link ChargingProfile} Data Transfer Object
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit and 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<Timer> 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<Timer> 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 + "]";
}
}

View File

@ -0,0 +1,96 @@
/**
* 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 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
+ "]";
}
}

View File

@ -0,0 +1,66 @@
/**
* 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 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<ChargingSession> 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<ChargingSession> getSessions() {
return sessions;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "ChargingSessions [total=" + total + ", numberOfSessions=" + numberOfSessions + ", chargingListState="
+ chargingListState + ", sessions=" + sessions + "]";
}
}

View File

@ -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;
}

View File

@ -16,10 +16,58 @@ package org.openhab.binding.mybmw.internal.dto.charge;
* 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 + "]";
}
}

View File

@ -0,0 +1,110 @@
/**
* 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 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
+ "]";
}
}

View File

@ -0,0 +1,77 @@
/**
* 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 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 + "]";
}
}

View File

@ -16,8 +16,30 @@ package org.openhab.binding.mybmw.internal.dto.charge;
* 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 + "]";
}
}

View File

@ -0,0 +1,81 @@
/**
* 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.ArrayList;
import java.util.List;
/**
*
* derived from the API responses
*
* @author Martin Grassl - initial contribution
*/
public class RemoteChargingCommands {
private List<String> chargingControl = new ArrayList<>();
private List<String> flapControl = new ArrayList<>();
private List<String> plugControl = new ArrayList<>();
/**
* @return the chargingControl
*/
public List<String> getChargingControl() {
return chargingControl;
}
/**
* @param chargingControl the chargingControl to set
*/
public void setChargingControl(List<String> chargingControl) {
this.chargingControl = chargingControl;
}
/**
* @return the flapControl
*/
public List<String> getFlapControl() {
return flapControl;
}
/**
* @param flapControl the flapControl to set
*/
public void setFlapControl(List<String> flapControl) {
this.flapControl = flapControl;
}
/**
* @return the plugControl
*/
public List<String> getPlugControl() {
return plugControl;
}
/**
* @param plugControl the plugControl to set
*/
public void setPlugControl(List<String> plugControl) {
this.plugControl = plugControl;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "RemoteChargingCommands [chargingControl=" + chargingControl + ", flapControl=" + flapControl
+ ", plugControl=" + plugControl + "]";
}
}

View File

@ -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 and 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 + "]";
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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;
}

View File

@ -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<CCM> checkControlMessages;
public List<CBS> serviceRequired;
public Location vehicleLocation;
public Tires tires;
// "climateControl":{} [todo] definition currently unknown
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -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"
}

View File

@ -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,

View File

@ -16,10 +16,49 @@ package org.openhab.binding.mybmw.internal.dto.remote;
* 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 + "]";
}
}

View File

@ -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"
}

View File

@ -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.
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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<DoorWindow> doorsAndWindows;// ":[
public List<CCMMessage> checkControlMessages;//
public List<CBSMessage> requiredServices;//
// "recallMessages":[],
// "recallExternalUrl":null,
public List<FuelIndicator> fuelIndicators;
public String timestampMessage;// ":"Updated from vehicle 12/21/2021 05:46 PM",
public ChargeProfile chargingProfile;
}

View File

@ -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 + "]";
}
}

View File

@ -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;
}

View File

@ -0,0 +1,75 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -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 + "]";
}
}

View File

@ -0,0 +1,67 @@
/**
* 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;
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<String> 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<String> getTimerWeekDays() {
return timerWeekDays;
}
public void setTimerWeekDays(List<String> 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 + "]";
}
}

View File

@ -0,0 +1,55 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,45 @@
/**
* 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 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 + "]";
}
}

View File

@ -0,0 +1,45 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,55 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,36 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,142 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,73 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -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 + "]";
}
}

View File

@ -0,0 +1,148 @@
/**
* 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;
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 + "]";
}
}

View File

@ -0,0 +1,47 @@
/**
* 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 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 + "]";
}
}

View File

@ -0,0 +1,224 @@
/**
* 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;
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 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;
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
*/
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;
}
/**
* returns a list of capabilities filtered by the provided suffix and the enabled requirement
*
* @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();
List<String> 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<String> getCapabilitiesAsStringList(String suffix, boolean compare) {
List<String> 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;
}
@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 + "]";
}
}

View File

@ -0,0 +1,101 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,54 @@
/**
* 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 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 "VehicleLocation [coordinates=" + coordinates + ", address=" + address + ", heading=" + heading + "]";
}
}

View File

@ -0,0 +1,45 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,231 @@
/**
* 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;
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<RequiredService> requiredServices = new ArrayList<>();
private List<CheckControlMessage> 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<ClimateTimer> 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<RequiredService> getRequiredServices() {
return requiredServices;
}
/**
* @return the checkControlMessages
*/
public List<CheckControlMessage> 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<ClimateTimer> 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;
}
}

View File

@ -0,0 +1,55 @@
/**
* 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;
/**
*
* 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;
}
}

View File

@ -0,0 +1,45 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,121 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,45 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,46 @@
/**
* 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;
/**
*
* 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
+ "]";
}
}

View File

@ -0,0 +1,64 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -0,0 +1,82 @@
/**
* 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;
/**
*
* 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 + "]";
}
}

View File

@ -15,19 +15,18 @@ package org.openhab.binding.mybmw.internal.handler;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
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.i18n.LocaleProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@ -40,102 +39,118 @@ import org.slf4j.Logger;
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 {
private final Logger logger = LoggerFactory.getLogger(MyBMWBridgeHandler.class);
private HttpClientFactory httpClientFactory;
private Optional<VehicleDiscovery> discoveryService = Optional.empty();
private Optional<MyBMWProxy> proxy = Optional.empty();
private Optional<ScheduledFuture<?>> initializerJob = Optional.empty();
private Optional<String> troubleshootFingerprint = Optional.empty();
private String localeLanguage;
public class MyBMWBridgeHandler extends BaseBridgeHandler {
public MyBMWBridgeHandler(Bridge bridge, HttpClientFactory hcf, String language) {
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<MyBMWProxy> myBmwProxy = Optional.empty();
private Optional<ScheduledFuture<?>> initializerJob = Optional.empty();
private Optional<VehicleDiscovery> vehicleDiscovery = Optional.empty();
private LocaleProvider localeProvider;
public MyBMWBridgeHandler(Bridge bridge, HttpClientFactory hcf, LocaleProvider localeProvider) {
super(bridge);
httpClientFactory = hcf;
localeLanguage = language;
this.localeProvider = localeProvider;
}
public void setVehicleDiscovery(VehicleDiscovery vehicleDiscovery) {
logger.trace("MyBMWBridgeHandler.setVehicleDiscovery");
this.vehicleDiscovery = Optional.of(vehicleDiscovery);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// no commands available
logger.trace("MyBMWBridgeHandler.handleCommand");
}
@Override
public void initialize() {
troubleshootFingerprint = Optional.empty();
logger.trace("MyBMWBridgeHandler.initialize");
updateStatus(ThingStatus.UNKNOWN);
MyBMWConfiguration config = getConfigAs(MyBMWConfiguration.class);
MyBMWBridgeConfiguration config = getConfigAs(MyBMWBridgeConfiguration.class);
if (config.language.equals(Constants.LANGUAGE_AUTODETECT)) {
config.language = localeLanguage;
config.language = localeProvider.getLocale().getLanguage().toLowerCase();
}
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);
if (environment == null) {
environment = "";
}
createMyBmwProxy(config, environment);
initializerJob = Optional.of(scheduler.schedule(this::discoverVehicles, 2, TimeUnit.SECONDS));
}
}
public static boolean checkConfiguration(MyBMWConfiguration config) {
if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) {
return false;
} else {
return BimmerConstants.EADRAX_SERVER_MAP.containsKey(config.region);
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));
}
logger.trace("MyBMWBridgeHandler proxy set");
}
}
@Override
public void dispose() {
logger.trace("MyBMWBridgeHandler.dispose");
initializerJob.ifPresent(job -> job.cancel(true));
}
public void requestVehicles() {
proxy.ifPresent(prox -> prox.requestVehicles(this));
public void vehicleDiscoveryError() {
logger.trace("MyBMWBridgeHandler.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("MyBMWBridgeHandler.vehicleDiscoverySuccess");
updateStatus(ThingStatus.ONLINE);
}
/**
* Response for vehicle request
*/
@Override
public synchronized void onResponse(@Nullable String response) {
if (response != null) {
updateStatus(ThingStatus.ONLINE);
List<Vehicle> vehicleList = Converter.getVehicleList(response);
discoveryService.get().onResponse(vehicleList);
troubleshootFingerprint = Optional.of(Converter.anonymousFingerprint(response));
logFingerPrint();
}
}
private void discoverVehicles() {
logger.trace("MyBMWBridgeHandler.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<Class<? extends ThingHandlerService>> getServices() {
return Set.of(VehicleDiscovery.class);
logger.trace("MyBMWBridgeHandler.getServices");
return List.of(VehicleDiscovery.class);
}
public Optional<MyBMWProxy> getProxy() {
return proxy;
}
public void setDiscoveryService(VehicleDiscovery discoveryService) {
this.discoveryService = Optional.of(discoveryService);
public Optional<MyBMWProxy> getMyBmwProxy() {
logger.trace("MyBMWBridgeHandler.getProxy");
createMyBmwProxy(getConfigAs(MyBMWBridgeConfiguration.class), ENVIRONMENT);
return myBmwProxy;
}
}

View File

@ -1,519 +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.openhab.core.util.StringUtils;
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 <a href="https://github.com/bimmerconnected/bimmer_connected">
* https://github.com/bimmerconnected/bimmer_connected</a>.
* File defining these constants
* <a href="https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py">
* https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py</a>
* <a href="https://customer.bmwgroup.com/one/app/oauth.js">https://customer.bmwgroup.com/one/app/oauth.js</a>
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit and send of charge profile
*/
@NonNullByDefault
public class MyBMWProxy {
private final Logger logger = LoggerFactory.getLogger(MyBMWProxy.class);
private Optional<RemoteServiceHandler> remoteServiceHandler = Optional.empty();
private final Token token = new Token();
private final HttpClient httpClient;
private final MyBMWConfiguration configuration;
/**
* URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py
*/
final String vehicleUrl;
final String remoteCommandUrl;
final String remoteStatusUrl;
final String serviceExecutionAPI = "/executeService";
final String serviceExecutionStateAPI = "/serviceExecutionStatus";
final String remoteServiceEADRXstatusUrl = BimmerConstants.API_REMOTE_SERVICE_BASE_URL
+ "eventStatus?eventId={event_id}";
public MyBMWProxy(HttpClientFactory httpClientFactory, MyBMWConfiguration config) {
httpClient = httpClientFactory.getCommonHttpClient();
configuration = config;
vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ BimmerConstants.API_VEHICLES;
remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ BimmerConstants.API_REMOTE_SERVICE_BASE_URL;
remoteStatusUrl = remoteCommandUrl + "eventStatus";
}
public synchronized void call(final String url, final boolean post, final @Nullable String encoding,
final @Nullable String params, final String brand, final ResponseCallback callback) {
// only executed in "simulation mode"
// SimulationTest.testSimulationOff() assures Injector is off when releasing
if (Injector.isActive()) {
if (url.equals(vehicleUrl)) {
((StringResponseCallback) callback).onResponse(Injector.getDiscovery());
} else if (url.endsWith(vehicleUrl)) {
((StringResponseCallback) callback).onResponse(Injector.getStatus());
} else {
logger.debug("Simulation of {} not supported", url);
}
return;
}
// return in case of unknown brand
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 responseCallback) {
responseCallback.onResponse(getContentAsString());
} else if (callback instanceof ByteResponseCallback responseCallback) {
responseCallback.onResponse(getContent());
} else {
logger.error("unexpected reponse type {}", callback.getClass().getName());
}
}
}
});
}
public void get(String url, @Nullable String coding, @Nullable String params, final String brand,
ResponseCallback callback) {
call(url, false, coding, params, brand, callback);
}
public void post(String url, @Nullable String coding, @Nullable String params, final String brand,
ResponseCallback callback) {
call(url, true, coding, params, brand, callback);
}
/**
* request all vehicles for one specific brand
*
* @param brand
* @param callback
*/
public void requestVehicles(String brand, StringResponseCallback callback) {
// calculate necessary parameters for query
MultiMap<String> vehicleParams = new MultiMap<String>();
vehicleParams.put(BimmerConstants.TIRE_GUARD_MODE, Constants.ENABLED);
vehicleParams.put(BimmerConstants.APP_DATE_TIME, Long.toString(System.currentTimeMillis()));
vehicleParams.put(BimmerConstants.APP_TIMEZONE, Integer.toString(Converter.getOffsetMinutes()));
String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false);
get(vehicleUrl + "?" + params, null, null, brand, callback);
}
/**
* request vehicles for all possible brands
*
* @param callback
*/
public void requestVehicles(StringResponseCallback callback) {
BimmerConstants.ALL_BRANDS.forEach(brand -> {
requestVehicles(brand, callback);
});
}
public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) {
final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ "/eadrax-ics/v3/presentation/vehicles/" + config.vin + "/images?carView=" + props.viewport;
get(localImageUrl, null, null, config.vehicleBrand, callback);
}
/**
* request charge statistics for electric vehicles
*
* @param callback
*/
public void requestChargeStatistics(VehicleConfiguration config, StringResponseCallback callback) {
MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
chargeStatisticsParams.put("vin", config.vin);
chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
String params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ "/eadrax-chs/v1/charging-statistics?" + params;
get(chargeStatisticsUrl, null, null, config.vehicleBrand, callback);
}
/**
* request charge statistics for electric vehicles
*
* @param callback
*/
public void requestChargeSessions(VehicleConfiguration config, StringResponseCallback callback) {
MultiMap<String> chargeSessionsParams = new MultiMap<String>();
chargeSessionsParams.put("vin", "WBY1Z81040V905639");
chargeSessionsParams.put("maxResults", "40");
chargeSessionsParams.put("include_date_picker", "true");
String params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ "/eadrax-chs/v1/charging-sessions?" + params;
get(chargeSessionsUrl, null, null, config.vehicleBrand, callback);
}
RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
remoteServiceHandler = Optional.of(new RemoteServiceHandler(vehicleHandler, this));
return remoteServiceHandler.get();
}
// Token handling
/**
* Gets new token if old one is expired or invalid. In case of error the token remains.
* So if token refresh fails the corresponding requests will also fail and update the
* Thing status accordingly.
*
* @return token
*/
public Token getToken() {
if (!token.isValid()) {
boolean tokenUpdateSuccess = false;
switch (configuration.region) {
case BimmerConstants.REGION_CHINA:
tokenUpdateSuccess = updateTokenChina();
break;
case BimmerConstants.REGION_NORTH_AMERICA:
tokenUpdateSuccess = updateToken();
break;
case BimmerConstants.REGION_ROW:
tokenUpdateSuccess = updateToken();
break;
default:
logger.warn("Region {} not supported", configuration.region);
break;
}
if (!tokenUpdateSuccess) {
logger.debug("Authorization failed!");
}
}
return token;
}
/**
* Everything is catched by surroundig try catch
* - HTTP Exceptions
* - JSONSyntax Exceptions
* - potential NullPointer Exceptions
*
* @return
*/
@SuppressWarnings("null")
public synchronized boolean updateToken() {
try {
/*
* Step 1) Get basic values for further queries
*/
String authValuesUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
+ BimmerConstants.API_OAUTH_CONFIG;
Request authValuesRequest = httpClient.newRequest(authValuesUrl).timeout(HTTP_TIMEOUT_SEC,
TimeUnit.SECONDS);
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 = StringUtils.getRandomAlphabetic(64).toLowerCase();
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 = StringUtils.getRandomAlphabetic(16).toLowerCase();
String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
MultiMap<String> baseParams = new MultiMap<String>();
baseParams.put(CLIENT_ID, aqr.clientId);
baseParams.put(RESPONSE_TYPE, CODE);
baseParams.put(REDIRECT_URI, aqr.returnUrl);
baseParams.put(STATE, state);
baseParams.put(NONCE, BimmerConstants.LOGIN_NONCE);
baseParams.put(SCOPE, String.join(Constants.SPACE, aqr.scopes));
baseParams.put(CODE_CHALLENGE, codeChallange);
baseParams.put(CODE_CHALLENGE_METHOD, "S256");
/**
* Step 3) Authorization with username and password
*/
String loginUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
Request loginRequest = httpClient.POST(loginUrl).timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS);
loginRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
MultiMap<String> loginParams = new MultiMap<String>(baseParams);
loginParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE);
loginParams.put(USERNAME, configuration.userName);
loginParams.put(PASSWORD, configuration.password);
loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
ContentResponse loginResponse = loginRequest.send();
if (loginResponse.getStatus() != 200) {
throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: "
+ loginResponse.getStatus() + ", Message: " + loginResponse.getContentAsString(),
loginResponse);
}
String authCode = getAuthCode(loginResponse.getContentAsString());
/**
* Step 4) Authorize with code
*/
Request authRequest = httpClient.POST(loginUrl).followRedirects(false).timeout(HTTP_TIMEOUT_SEC,
TimeUnit.SECONDS);
MultiMap<String> authParams = new MultiMap<String>(baseParams);
authParams.put(AUTHORIZATION, authCode);
authRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
ContentResponse authResponse = authRequest.send();
if (authResponse.getStatus() != 302) {
throw new HttpResponseException("URL: " + authRequest.getURI() + ", Error: " + authResponse.getStatus()
+ ", Message: " + authResponse.getContentAsString(), authResponse);
}
String code = MyBMWProxy.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
/**
* Step 5) Request token
*/
Request codeRequest = httpClient.POST(aqr.tokenEndpoint).timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS);
String basicAuth = "Basic "
+ Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
codeRequest.header(AUTHORIZATION, basicAuth);
MultiMap<String> codeParams = new MultiMap<String>();
codeParams.put(CODE, code);
codeParams.put(CODE_VERIFIER, codeVerifier);
codeParams.put(REDIRECT_URI, aqr.returnUrl);
codeParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE);
codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
ContentResponse codeResponse = codeRequest.send();
if (codeResponse.getStatus() != 200) {
throw new HttpResponseException("URL: " + codeRequest.getURI() + ", Error: " + codeResponse.getStatus()
+ ", Message: " + codeResponse.getContentAsString(), codeResponse);
}
AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class);
token.setType(ar.tokenType);
token.setToken(ar.accessToken);
token.setExpiration(ar.expiresIn);
return true;
} catch (Exception e) {
logger.warn("Authorization Exception: {}", e.getMessage());
}
return false;
}
private String getAuthCode(String response) {
String[] keys = response.split("&");
for (int i = 0; i < keys.length; i++) {
if (keys[i].startsWith(AUTHORIZATION)) {
String authCode = keys[i].split("=")[1];
authCode = authCode.split("\"")[0];
return authCode;
}
}
return Constants.EMPTY;
}
public static String codeFromUrl(String encodedUrl) {
final MultiMap<String> tokenMap = new MultiMap<String>();
UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
final StringBuilder codeFound = new StringBuilder();
tokenMap.forEach((key, value) -> {
if (!value.isEmpty()) {
String val = value.get(0);
if (key.endsWith(CODE)) {
codeFound.append(val);
}
}
});
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).timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS);
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).timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS);
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;
}
}

View File

@ -0,0 +1,164 @@
/**
* 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 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 and 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<ScheduledFuture<?>> stateJob = Optional.empty();
private Optional<String> serviceExecuting = Optional.empty();
private Optional<String> 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();
});
}
}
}

View File

@ -1,228 +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 <a href="https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py">
* https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/remote_services.py</a>
*
* @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit and send of charge profile
*/
@NonNullByDefault
public class RemoteServiceHandler implements StringResponseCallback {
private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class);
private static final String EVENT_ID = "eventId";
private static final String DATA = "data";
private static final int GIVEUP_COUNTER = 12; // after 12 retries the state update will give up
private static final int STATE_UPDATE_SEC = HTTPConstants.HTTP_TIMEOUT_SEC + 1; // regular timeout + 1sec
private final MyBMWProxy proxy;
private final VehicleHandler handler;
private final String serviceExecutionAPI;
private final String serviceExecutionStateAPI;
private int counter = 0;
private Optional<ScheduledFuture<?>> stateJob = Optional.empty();
private Optional<String> serviceExecuting = Optional.empty();
private Optional<String> executingEventId = Optional.empty();
public enum ExecutionState {
READY,
INITIATED,
PENDING,
DELIVERED,
EXECUTED,
ERROR,
TIMEOUT
}
public enum RemoteService {
LIGHT_FLASH("Flash Lights", REMOTE_SERVICE_LIGHT_FLASH, REMOTE_SERVICE_LIGHT_FLASH),
VEHICLE_FINDER("Vehicle Finder", REMOTE_SERVICE_VEHICLE_FINDER, REMOTE_SERVICE_VEHICLE_FINDER),
DOOR_LOCK("Door Lock", REMOTE_SERVICE_DOOR_LOCK, REMOTE_SERVICE_DOOR_LOCK),
DOOR_UNLOCK("Door Unlock", REMOTE_SERVICE_DOOR_UNLOCK, REMOTE_SERVICE_DOOR_UNLOCK),
HORN_BLOW("Horn Blow", REMOTE_SERVICE_HORN, REMOTE_SERVICE_HORN),
CLIMATE_NOW_START("Start Climate", REMOTE_SERVICE_AIR_CONDITIONING_START, "climate-now?action=START"),
CLIMATE_NOW_STOP("Stop Climate", REMOTE_SERVICE_AIR_CONDITIONING_STOP, "climate-now?action=STOP");
private final String label;
private final String id;
private final String command;
RemoteService(final String label, final String id, String command) {
this.label = label;
this.id = id;
this.command = command;
}
public String getLabel() {
return label;
}
public String getId() {
return id;
}
public String getCommand() {
return command;
}
}
public RemoteServiceHandler(VehicleHandler vehicleHandler, MyBMWProxy myBmwProxy) {
handler = vehicleHandler;
proxy = myBmwProxy;
final VehicleConfiguration config = handler.getConfiguration().get();
serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/";
serviceExecutionStateAPI = proxy.remoteStatusUrl;
}
boolean execute(RemoteService service, String... data) {
synchronized (this) {
if (serviceExecuting.isPresent()) {
logger.debug("Execution rejected - {} still pending", serviceExecuting.get());
// only one service executing
return false;
}
serviceExecuting = Optional.of(service.getId());
}
final MultiMap<String> dataMap = new MultiMap<String>();
if (data.length > 0) {
dataMap.add(DATA, data[0]);
proxy.post(serviceExecutionAPI + service.getCommand(), CONTENT_TYPE_JSON_ENCODED, data[0],
handler.getConfiguration().get().vehicleBrand, this);
} else {
proxy.post(serviceExecutionAPI + service.getCommand(), null, null,
handler.getConfiguration().get().vehicleBrand, this);
}
return true;
}
public void getState() {
synchronized (this) {
serviceExecuting.ifPresentOrElse(service -> {
if (counter >= GIVEUP_COUNTER) {
logger.warn("Giving up updating state for {} after {} times", service, GIVEUP_COUNTER);
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
ExecutionState.TIMEOUT.name().toLowerCase());
reset();
// immediately refresh data
handler.getData();
} else {
counter++;
final MultiMap<String> dataMap = new MultiMap<String>();
dataMap.add(EVENT_ID, executingEventId.get());
final String encoded = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false);
proxy.post(serviceExecutionStateAPI + Constants.QUESTION + encoded, null, null,
handler.getConfiguration().get().vehicleBrand, this);
}
}, () -> {
logger.warn("No Service executed to get state");
});
stateJob = Optional.empty();
}
}
@Override
public void onResponse(@Nullable String result) {
if (result != null) {
try {
ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class);
if (esc != null) {
if (esc.eventId != null) {
// service initiated - store event id for further MyBMW updates
executingEventId = Optional.of(esc.eventId);
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
ExecutionState.INITIATED.name().toLowerCase());
} else if (esc.eventStatus != null) {
// service status updated
synchronized (this) {
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
esc.eventStatus.toLowerCase());
if (ExecutionState.EXECUTED.name().equalsIgnoreCase(esc.eventStatus)
|| ExecutionState.ERROR.name().equalsIgnoreCase(esc.eventStatus)) {
// refresh loop ends - update of status handled in the normal refreshInterval.
// Earlier update doesn't show better results!
reset();
return;
}
}
}
}
} catch (JsonSyntaxException jse) {
logger.debug("RemoteService response is unparseable: {} {}", result, jse.getMessage());
}
}
// schedule even if no result is present until retries exceeded
synchronized (this) {
stateJob.ifPresent(job -> {
if (!job.isDone()) {
job.cancel(true);
}
});
stateJob = Optional.of(handler.getScheduler().schedule(this::getState, STATE_UPDATE_SEC, TimeUnit.SECONDS));
}
}
@Override
public void onError(NetworkError error) {
synchronized (this) {
handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null),
ExecutionState.ERROR.name().toLowerCase() + Constants.SPACE + Integer.toString(error.status));
reset();
}
}
private void reset() {
serviceExecuting = Optional.empty();
executingEventId = Optional.empty();
counter = 0;
}
public void cancel() {
synchronized (this) {
stateJob.ifPresent(action -> {
if (!action.isDone()) {
action.cancel(true);
}
stateJob = Optional.empty();
});
}
}
}

View File

@ -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 {
void onError(NetworkError error);
}

View File

@ -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 {
void onResponse(@Nullable String result);
}

View File

@ -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 and send of charge profile
*/
@NonNullByDefault
public abstract class VehicleChannelHandler extends BaseThingHandler {
protected final Logger logger = LoggerFactory.getLogger(VehicleChannelHandler.class);
protected boolean hasFuel = false;
protected boolean isElectric = false;
protected boolean isHybrid = false;
// List Interfaces
protected List<CBS> serviceList = new ArrayList<CBS>();
protected String selectedService = Constants.UNDEF;
protected List<CCMMessage> checkControlList = new ArrayList<CCMMessage>();
protected String selectedCC = Constants.UNDEF;
protected List<ChargeSession> sessionList = new ArrayList<ChargeSession>();
protected String selectedSession = Constants.UNDEF;
protected MyBMWCommandOptionProvider commandOptionProvider;
private LocationProvider locationProvider;
// Data Caches
protected Optional<String> vehicleStatusCache = Optional.empty();
protected Optional<byte[]> 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<CommandOption> options) {
commandOptionProvider.setCommandOptions(new ChannelUID(thing.getUID(), group, id), options);
}
protected void updateChannel(final String group, final String id, final State state) {
updateState(new ChannelUID(thing.getUID(), group, id), state);
}
protected void updateChargeStatistics(ChargeStatisticsContainer csc) {
updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, TITLE, StringType.valueOf(csc.description));
updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, ENERGY,
QuantityType.valueOf(csc.statistics.totalEnergyCharged, Units.KILOWATT_HOUR));
updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, SESSIONS,
DecimalType.valueOf(Integer.toString(csc.statistics.numberOfChargingSessions)));
}
protected void updateVehicle(Vehicle v) {
updateVehicleStatus(v);
updateRange(v);
updateDoors(v.properties.doorsAndWindows);
updateWindows(v.properties.doorsAndWindows);
updatePosition(v.properties.vehicleLocation);
updateServices(v.properties.serviceRequired);
updateCheckControls(v.status.checkControlMessages);
updateTires(v.properties.tires);
}
private void updateTires(@Nullable Tires tires) {
if (tires == null) {
updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT, UnDefType.UNDEF);
updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET, UnDefType.UNDEF);
} else {
updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT,
QuantityType.valueOf(tires.frontLeft.status.currentPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET,
QuantityType.valueOf(tires.frontLeft.status.targetPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT,
QuantityType.valueOf(tires.frontRight.status.currentPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET,
QuantityType.valueOf(tires.frontRight.status.targetPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT,
QuantityType.valueOf(tires.rearLeft.status.currentPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET,
QuantityType.valueOf(tires.rearLeft.status.targetPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT,
QuantityType.valueOf(tires.rearRight.status.currentPressure / 100, Units.BAR));
updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET,
QuantityType.valueOf(tires.rearRight.status.targetPressure / 100, Units.BAR));
}
}
protected void updateVehicleStatus(Vehicle v) {
updateChannel(CHANNEL_GROUP_STATUS, LOCK, Converter.getLockState(v.properties.areDoorsLocked));
updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE,
VehicleStatusUtils.getNextServiceDate(v.properties.serviceRequired));
updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE,
VehicleStatusUtils.getNextServiceMileage(v.properties.serviceRequired));
updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL,
StringType.valueOf(v.status.checkControlMessagesGeneralState));
updateChannel(CHANNEL_GROUP_STATUS, MOTION, OnOffType.from(v.properties.inMotion));
updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
DateTimeType.valueOf(Converter.zonedToLocalDateTime(v.properties.lastUpdatedAt)));
updateChannel(CHANNEL_GROUP_STATUS, DOORS, Converter.getClosedState(v.properties.areDoorsClosed));
updateChannel(CHANNEL_GROUP_STATUS, WINDOWS, Converter.getClosedState(v.properties.areWindowsClosed));
if (isElectric) {
updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION,
Converter.getConnectionState(v.properties.chargingState.isChargerConnected));
updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
StringType.valueOf(Converter.toTitleCase(VehicleStatusUtils.getChargStatus(v))));
updateChannel(CHANNEL_GROUP_STATUS, CHARGE_INFO,
StringType.valueOf(Converter.getLocalTime(VehicleStatusUtils.getChargeInfo(v))));
}
}
protected void updateRange(Vehicle v) {
// get the right unit
Unit<Length> lengthUnit = VehicleStatusUtils.getLengthUnit(v.status.fuelIndicators);
if (lengthUnit == null) {
return;
}
if (isElectric) {
int rangeElectric = VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, v);
QuantityType<Length> qtElectricRange = QuantityType.valueOf(rangeElectric, lengthUnit);
QuantityType<Length> qtElectricRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeElectric),
lengthUnit);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC, qtElectricRange);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC, qtElectricRadius);
}
if (hasFuel) {
int rangeFuel = VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, v);
QuantityType<Length> qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit);
QuantityType<Length> qtFuelRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeFuel), lengthUnit);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, qtFuelRange);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL, qtFuelRadius);
}
if (isHybrid) {
int rangeCombined = VehicleStatusUtils.getRange(Constants.PHEV, v);
QuantityType<Length> qtHybridRange = QuantityType.valueOf(rangeCombined, lengthUnit);
QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeCombined),
lengthUnit);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID, qtHybridRange);
updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID, qtHybridRadius);
}
if (v.status.currentMileage.mileage == Constants.INT_UNDEF) {
updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, UnDefType.UNDEF);
} else {
updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
QuantityType.valueOf(v.status.currentMileage.mileage, lengthUnit));
}
if (isElectric) {
updateChannel(CHANNEL_GROUP_RANGE, SOC,
QuantityType.valueOf(v.properties.chargingState.chargePercentage, Units.PERCENT));
}
if (hasFuel) {
updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
QuantityType.valueOf(v.properties.fuelLevel.value, Units.LITRE));
}
}
protected void updateCheckControls(List<CCMMessage> ccl) {
if (ccl.isEmpty()) {
// No Check Control available - show not active
CCMMessage ccm = new CCMMessage();
ccm.title = Constants.NO_ENTRIES;
ccm.longDescription = Constants.NO_ENTRIES;
ccm.state = Constants.NO_ENTRIES;
ccl.add(ccm);
}
// add all elements to options
checkControlList = ccl;
List<CommandOption> ccmDescriptionOptions = new ArrayList<>();
boolean isSelectedElementIn = false;
int index = 0;
for (CCMMessage ccEntry : checkControlList) {
ccmDescriptionOptions.add(new CommandOption(Integer.toString(index), ccEntry.title));
if (selectedCC.equals(ccEntry.title)) {
isSelectedElementIn = true;
}
index++;
}
setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
// if current selected item isn't anymore in the list select first entry
if (!isSelectedElementIn) {
selectCheckControl(0);
}
}
protected void selectCheckControl(int index) {
if (index >= 0 && index < checkControlList.size()) {
CCMMessage ccEntry = checkControlList.get(index);
selectedCC = ccEntry.title;
updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, StringType.valueOf(ccEntry.title));
updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, StringType.valueOf(ccEntry.longDescription));
updateChannel(CHANNEL_GROUP_CHECK_CONTROL, SEVERITY, StringType.valueOf(ccEntry.state));
}
}
protected void updateServices(List<CBS> sl) {
// if list is empty add "undefined" element
if (sl.isEmpty()) {
CBS cbsm = new CBS();
cbsm.type = Constants.NO_ENTRIES;
sl.add(cbsm);
}
// add all elements to options
serviceList = sl;
List<CommandOption> serviceNameOptions = new ArrayList<>();
boolean isSelectedElementIn = false;
int index = 0;
for (CBS serviceEntry : serviceList) {
// create StateOption with "value = list index" and "label = human readable string"
serviceNameOptions.add(new CommandOption(Integer.toString(index), serviceEntry.type));
if (selectedService.equals(serviceEntry.type)) {
isSelectedElementIn = true;
}
index++;
}
setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
// if current selected item isn't anymore in the list select first entry
if (!isSelectedElementIn) {
selectService(0);
}
}
protected void selectService(int index) {
if (index >= 0 && index < serviceList.size()) {
CBS serviceEntry = serviceList.get(index);
selectedService = serviceEntry.type;
updateChannel(CHANNEL_GROUP_SERVICE, NAME, StringType.valueOf(Converter.toTitleCase(serviceEntry.type)));
if (serviceEntry.dateTime != null) {
updateChannel(CHANNEL_GROUP_SERVICE, DATE,
DateTimeType.valueOf(Converter.zonedToLocalDateTime(serviceEntry.dateTime)));
} else {
updateChannel(CHANNEL_GROUP_SERVICE, DATE, UnDefType.UNDEF);
}
if (serviceEntry.distance != null) {
if (Constants.KILOMETERS_JSON.equals(serviceEntry.distance.units)) {
updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
QuantityType.valueOf(serviceEntry.distance.value, Constants.KILOMETRE_UNIT));
} else {
updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
QuantityType.valueOf(serviceEntry.distance.value, ImperialUnits.MILE));
}
} else {
updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT));
}
}
}
protected void updateSessions(List<ChargeSession> sl) {
// if list is empty add "undefined" element
if (sl.isEmpty()) {
ChargeSession cs = new ChargeSession();
cs.title = Constants.NO_ENTRIES;
sl.add(cs);
}
// add all elements to options
sessionList = sl;
List<CommandOption> sessionNameOptions = new ArrayList<>();
boolean isSelectedElementIn = false;
int index = 0;
for (ChargeSession session : sessionList) {
// create StateOption with "value = list index" and "label = human readable string"
sessionNameOptions.add(new CommandOption(Integer.toString(index), session.title));
if (selectedService.equals(session.title)) {
isSelectedElementIn = true;
}
index++;
}
setOptions(CHANNEL_GROUP_CHARGE_SESSION, TITLE, sessionNameOptions);
// if current selected item isn't anymore in the list select first entry
if (!isSelectedElementIn) {
selectSession(0);
}
}
protected void selectSession(int index) {
if (index >= 0 && index < sessionList.size()) {
ChargeSession sessionEntry = sessionList.get(index);
selectedService = sessionEntry.title;
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, TITLE, StringType.valueOf(sessionEntry.title));
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, SUBTITLE, StringType.valueOf(sessionEntry.subtitle));
if (sessionEntry.energyCharged != null) {
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(sessionEntry.energyCharged));
} else {
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(Constants.UNDEF));
}
if (sessionEntry.issues != null) {
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(sessionEntry.issues));
} else {
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(Constants.HYPHEN));
}
updateChannel(CHANNEL_GROUP_CHARGE_SESSION, STATUS, StringType.valueOf(sessionEntry.sessionStatus));
}
}
protected void updateChargeProfile(ChargeProfile cp) {
ChargeProfileWrapper cpw = new ChargeProfileWrapper(cp);
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_PREFERENCE, StringType.valueOf(cpw.getPreference()));
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_MODE, StringType.valueOf(cpw.getMode()));
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CONTROL, StringType.valueOf(cpw.getControlType()));
ChargingSettings cs = cpw.getChargeSettings();
if (cs != null) {
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_TARGET,
DecimalType.valueOf(Integer.toString(cs.targetSoc)));
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_LIMIT,
OnOffType.from(cs.isAcCurrentLimitActive));
}
final Boolean climate = cpw.isEnabled(ProfileKey.CLIMATE);
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CLIMATE,
climate == null ? UnDefType.UNDEF : OnOffType.from(climate));
updateTimedState(cpw, ProfileKey.WINDOWSTART);
updateTimedState(cpw, ProfileKey.WINDOWEND);
updateTimedState(cpw, ProfileKey.TIMER1);
updateTimedState(cpw, ProfileKey.TIMER2);
updateTimedState(cpw, ProfileKey.TIMER3);
updateTimedState(cpw, ProfileKey.TIMER4);
}
protected void updateTimedState(ChargeProfileWrapper profile, ProfileKey key) {
final TimedChannel timed = ChargeProfileUtils.getTimedChannel(key);
if (timed != null) {
final LocalTime time = profile.getTime(key);
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.time,
time.equals(Constants.NULL_LOCAL_TIME) ? UnDefType.UNDEF
: new DateTimeType(ZonedDateTime.of(Constants.EPOCH_DAY, time, ZoneId.systemDefault())));
if (timed.timer != null) {
final Boolean enabled = profile.isEnabled(key);
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.timer + CHARGE_ENABLED,
enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled));
if (timed.hasDays) {
final Set<DayOfWeek> days = profile.getDays(key);
EnumSet.allOf(DayOfWeek.class).forEach(day -> {
updateChannel(CHANNEL_GROUP_CHARGE_PROFILE,
timed.timer + ChargeProfileUtils.getDaysChannel(day),
days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)));
});
}
}
}
}
protected void updateDoors(DoorsWindows dw) {
updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT,
StringType.valueOf(Converter.toTitleCase(dw.doors.driverFront)));
updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR,
StringType.valueOf(Converter.toTitleCase(dw.doors.driverRear)));
updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT,
StringType.valueOf(Converter.toTitleCase(dw.doors.passengerFront)));
updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR,
StringType.valueOf(Converter.toTitleCase(dw.doors.passengerRear)));
updateChannel(CHANNEL_GROUP_DOORS, TRUNK, StringType.valueOf(Converter.toTitleCase(dw.trunk)));
updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(dw.hood)));
}
protected void updateWindows(DoorsWindows dw) {
updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT,
StringType.valueOf(Converter.toTitleCase(dw.windows.driverFront)));
updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR,
StringType.valueOf(Converter.toTitleCase(dw.windows.driverRear)));
updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT,
StringType.valueOf(Converter.toTitleCase(dw.windows.passengerFront)));
updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR,
StringType.valueOf(Converter.toTitleCase(dw.windows.passengerRear)));
updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, StringType.valueOf(Converter.toTitleCase(dw.moonroof)));
}
protected void updatePosition(Location pos) {
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);
}
}
}
}

View File

@ -0,0 +1,379 @@
/**
* 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.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.core.util.StringUtils;
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:
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 caught by surrounding try catch
* - HTTP Exceptions
* - JSONSyntax Exceptions
* - potential NullPointer Exceptions
*
* @return true if the token was successfully updated
*/
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 = StringUtils.getRandomAlphabetic(64).toLowerCase();
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 = StringUtils.getRandomAlphabetic(64).toLowerCase();
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;
}
}

View File

@ -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;

View File

@ -0,0 +1,97 @@
/**
* 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 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<VehicleBase> getVehicleBaseList(String vehicleBaseJson) {
try {
VehicleBase[] vehicleBaseArray = deserializeString(vehicleBaseJson, VehicleBase[].class);
return Arrays.asList(vehicleBaseArray);
} catch (JsonSyntaxException e) {
LOGGER.debug("JsonSyntaxException {}", e.getMessage());
return new ArrayList<VehicleBase>();
}
}
public static VehicleStateContainer getVehicleState(String vehicleStateJson) {
try {
VehicleStateContainer vehicleState = deserializeString(vehicleStateJson, VehicleStateContainer.class);
vehicleState.setRawStateJson(vehicleStateJson);
return vehicleState;
} catch (JsonSyntaxException e) {
LOGGER.debug("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.debug("JsonSyntaxException {}", e.getMessage());
return new ChargingStatisticsContainer();
}
}
public static ChargingSessionsContainer getChargingSessions(String chargeSessionsJson) {
try {
return deserializeString(chargeSessionsJson, ChargingSessionsContainer.class);
} catch (JsonSyntaxException e) {
LOGGER.debug("JsonSyntaxException {}", e.getMessage());
return new ChargingSessionsContainer();
}
}
public static ExecutionStatusContainer getExecutionStatus(String executionStatusJson) {
try {
return deserializeString(executionStatusJson, ExecutionStatusContainer.class);
} catch (JsonSyntaxException e) {
LOGGER.debug("JsonSyntaxException {}", e.getMessage());
return new ExecutionStatusContainer();
}
}
public static <T> T deserializeString(String toBeDeserialized, Class<T> deserializedClass) {
return GSON.fromJson(toBeDeserialized, deserializedClass);
}
}

View File

@ -0,0 +1,201 @@
/**
* 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 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<VehicleBase> 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<VehicleBase> requestVehiclesBase() throws NetworkException {
List<VehicleBase> 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 "";
}
}
}

View File

@ -0,0 +1,413 @@
/**
* 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.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 and 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";
logger.trace("MyBMWHttpProxy - ready");
}
@Override
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();
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
* @return the vehicles of one brand
*/
public List<VehicleBase> 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());
return vehicleResponseString;
}
/**
* request vehicles for all possible brands
*
* @return the list of vehicles
*/
public List<VehicleBase> requestVehiclesBase() throws NetworkException {
List<VehicleBase> vehicles = new ArrayList<>();
for (String brand : BimmerConstants.REQUESTED_BRANDS) {
try {
vehicles.addAll(requestVehiclesBase(brand));
Thread.sleep(10000);
} catch (Exception e) {
logger.warn("error retrieving the base vehicles for brand {}: {}", brand, e.getMessage());
}
}
return vehicles;
}
/**
* request the vehicle image
*
* @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)
+ "/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 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());
return vehicleStateResponseString;
}
/**
* 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);
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
*
* @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);
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;
}
/**
* 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();
byte[] response = post(executionUrl, brand, vin, HTTPConstants.CONTENT_TYPE_JSON, service.getBody());
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;
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 brand
* @param vin
* @param contentType
* @return byte array of the response body
*/
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 brand
* @param vin
* @param contentType
* @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 {
return call(url, true, brand, vin, contentType, body);
}
/**
* executes the real call to the backend
*
* @param url
* @param post boolean value indicating if it is a post request
* @param brand
* @param vin
* @param contentType
* @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 {
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.toLowerCase(),
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 (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);
}
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 ######");
}
}

View File

@ -0,0 +1,96 @@
/**
* 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 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<VehicleBase> requestVehiclesBase(String brand) throws NetworkException;
String requestVehiclesBaseJson(String brand) throws NetworkException;
/**
* request vehicles for all possible brands
*
* @param callback
*/
List<VehicleBase> 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;
}

View File

@ -0,0 +1,102 @@
/**
* 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 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 + "]";
}
}

View File

@ -0,0 +1,245 @@
/**
* 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 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
* <p>
* - vin
* </p>
* <p>
* - location
* </p>
*
* @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;
}
}

View File

@ -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 {
void onResponse(byte[] result);
public enum ExecutionState {
READY,
INITIATED,
PENDING,
DELIVERED,
EXECUTED,
ERROR,
TIMEOUT
}

View File

@ -0,0 +1,71 @@
/**
* 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.enums;
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;
/**
*
* 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, "start-charging", "");
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;
}
}

View File

@ -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;
}
}

View File

@ -27,49 +27,58 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* <a href="https://customer.bmwgroup.com/one/app/oauth.js">https://customer.bmwgroup.com/one/app/oauth.js</a>
*
* @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<String> 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<String> 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<String, String> 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<String, String> EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, EADRAX_SERVER_NORTH_AMERICA,
REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW);
public static final String OCP_APIM_KEY_NORTH_AMERICA = "31e102f5-6f7e-7ef3-9044-ddce63891362";
public static final String OCP_APIM_KEY_ROW = "4f1c85a3-758f-a37d-bbb6-f8704494acfa";
public static final Map<String, String> OCP_APIM_KEYS = Map.of(REGION_NORTH_AMERICA, OCP_APIM_KEY_NORTH_AMERICA,
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<String, String> OCP_APIM_KEYS = Map.of(REGION_NORTH_AMERICA, OCP_APIM_KEY_NORTH_AMERICA,
REGION_ROW, OCP_APIM_KEY_ROW);
public static final String CHINA_PUBLIC_KEY = "/eadrax-coas/v1/cop/publickey";
public static final String CHINA_LOGIN = "/eadrax-coas/v1/login/pwd";
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<String, String> 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";
}

View File

@ -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<ChargingMode> mode = Optional.empty();
private Optional<ChargingPreference> preference = Optional.empty();
private Optional<String> controlType = Optional.empty();
private Optional<ChargingSettings> chargeSettings = Optional.empty();
private final Map<ProfileKey, Boolean> enabled = new HashMap<>();
private final Map<ProfileKey, LocalTime> times = new HashMap<>();
private final Map<ProfileKey, Set<DayOfWeek>> daysOfWeek = new HashMap<>();
public ChargeProfileWrapper(final ChargeProfile profile) {
setPreference(profile.chargingPreference);
setMode(profile.chargingMode);
controlType = Optional.of(profile.chargingControlType);
chargeSettings = Optional.of(profile.chargingSettings);
setEnabled(CLIMATE, profile.climatisationOn);
addTimer(TIMER1, profile.getTimerId(1));
addTimer(TIMER2, profile.getTimerId(2));
if (profile.chargingControlType.equals(WEEKLY_PLANNER)) {
addTimer(TIMER3, profile.getTimerId(3));
addTimer(TIMER4, profile.getTimerId(4));
}
if (CHARGING_WINDOW.equals(profile.chargingPreference)) {
addTime(WINDOWSTART, profile.reductionOfChargeCurrent.start);
addTime(WINDOWEND, profile.reductionOfChargeCurrent.end);
} else {
preference.ifPresent(pref -> {
if (ChargingPreference.chargingWindow.equals(pref)) {
addTime(WINDOWSTART, null);
addTime(WINDOWEND, null);
}
});
}
}
public @Nullable Boolean isEnabled(final ProfileKey key) {
return enabled.get(key);
}
public void setEnabled(final ProfileKey key, @Nullable final Boolean enabled) {
if (enabled == null) {
this.enabled.remove(key);
} else {
this.enabled.put(key, enabled);
}
}
public @Nullable String getMode() {
return mode.map(m -> m.name()).orElse(null);
}
public @Nullable String getControlType() {
return controlType.get();
}
public @Nullable ChargingSettings getChargeSettings() {
return chargeSettings.get();
}
public void setMode(final @Nullable String mode) {
if (mode != null) {
try {
this.mode = Optional.of(ChargingMode.valueOf(mode));
return;
} catch (IllegalArgumentException iae) {
LOGGER.warn("unexpected value for chargingMode: {}", mode);
}
}
this.mode = Optional.empty();
}
public @Nullable String getPreference() {
return preference.map(pref -> pref.name()).orElse(null);
}
public void setPreference(final @Nullable String preference) {
if (preference != null) {
try {
this.preference = Optional.of(ChargingPreference.valueOf(preference));
return;
} catch (IllegalArgumentException iae) {
LOGGER.warn("unexpected value for chargingPreference: {}", preference);
}
}
this.preference = Optional.empty();
}
public @Nullable Set<DayOfWeek> getDays(final ProfileKey key) {
return daysOfWeek.get(key);
}
public void setDays(final ProfileKey key, final @Nullable Set<DayOfWeek> days) {
if (days == null) {
daysOfWeek.remove(key);
} else {
daysOfWeek.put(key, days);
}
}
public void setDayEnabled(final ProfileKey key, final DayOfWeek day, final boolean enabled) {
final Set<DayOfWeek> days = daysOfWeek.get(key);
if (days == null) {
daysOfWeek.put(key, enabled ? EnumSet.of(day) : EnumSet.noneOf(DayOfWeek.class));
} else {
if (enabled) {
days.add(day);
} else {
days.remove(day);
}
}
}
public LocalTime getTime(final ProfileKey key) {
LocalTime t = times.get(key);
if (t != null) {
return t;
} else {
LOGGER.debug("Profile not valid - Key {} doesn't contain boolean value", key);
return Constants.NULL_LOCAL_TIME;
}
}
public void setTime(final ProfileKey key, @Nullable LocalTime time) {
if (time == null) {
times.remove(key);
} else {
times.put(key, time);
}
}
public String getJson() {
final ChargeProfile profile = new ChargeProfile();
preference.ifPresent(pref -> profile.chargingPreference = pref.name());
profile.chargingControlType = controlType.get();
Boolean enabledBool = isEnabled(CLIMATE);
profile.climatisationOn = enabledBool == null ? false : enabledBool;
preference.ifPresent(pref -> {
if (ChargingPreference.chargingWindow.equals(pref)) {
profile.chargingMode = getMode();
final LocalTime start = getTime(WINDOWSTART);
final LocalTime end = getTime(WINDOWEND);
if (!start.equals(Constants.NULL_LOCAL_TIME) && !end.equals(Constants.NULL_LOCAL_TIME)) {
ChargingWindow cw = new ChargingWindow();
profile.reductionOfChargeCurrent = cw;
cw.start = new Time();
cw.start.hour = start.getHour();
cw.start.minute = start.getMinute();
cw.end = new Time();
cw.end.hour = end.getHour();
cw.end.minute = end.getMinute();
}
}
});
profile.departureTimes = new ArrayList<Timer>();
profile.departureTimes.add(getTimer(TIMER1));
profile.departureTimes.add(getTimer(TIMER2));
if (profile.chargingControlType.equals(WEEKLY_PLANNER)) {
profile.departureTimes.add(getTimer(TIMER3));
profile.departureTimes.add(getTimer(TIMER4));
}
profile.chargingSettings = chargeSettings.get();
return Converter.getGson().toJson(profile);
}
private void addTime(final ProfileKey key, @Nullable final Time time) {
try {
times.put(key, time == null ? NULL_LOCAL_TIME : LocalTime.parse(Converter.getTime(time), TIME_FORMATER));
} catch (DateTimeParseException dtpe) {
LOGGER.warn("unexpected value for {} time: {}", key.name(), time);
}
}
private void addTimer(final ProfileKey key, @Nullable final Timer timer) {
if (timer == null) {
enabled.put(key, false);
addTime(key, null);
daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class));
} else {
enabled.put(key, ACTIVATE.equals(timer.action));
addTime(key, timer.timeStamp);
final EnumSet<DayOfWeek> daySet = EnumSet.noneOf(DayOfWeek.class);
if (timer.timerWeekDays != null) {
daysOfWeek.put(key, EnumSet.noneOf(DayOfWeek.class));
for (String day : timer.timerWeekDays) {
try {
daySet.add(DayOfWeek.valueOf(day.toUpperCase()));
} catch (IllegalArgumentException iae) {
LOGGER.warn("unexpected value for {} day: {}", key.name(), day);
}
daysOfWeek.put(key, daySet);
}
}
}
}
private Timer getTimer(final ProfileKey key) {
final Timer timer = new Timer();
switch (key) {
case TIMER1:
timer.id = 1;
break;
case TIMER2:
timer.id = 2;
break;
case TIMER3:
timer.id = 3;
break;
case TIMER4:
timer.id = 4;
break;
default:
// timer id stays -1
break;
}
Boolean enabledBool = isEnabled(key);
if (enabledBool != null) {
timer.action = enabledBool ? ACTIVATE : DEACTIVATE;
} else {
timer.action = DEACTIVATE;
}
final LocalTime time = getTime(key);
if (!time.equals(Constants.NULL_LOCAL_TIME)) {
timer.timeStamp = new Time();
timer.timeStamp.hour = time.getHour();
timer.timeStamp.minute = time.getMinute();
}
final Set<DayOfWeek> days = daysOfWeek.get(key);
if (days != null) {
timer.timerWeekDays = new ArrayList<>();
for (DayOfWeek day : days) {
timer.timerWeekDays.add(day.name().toLowerCase());
}
}
return timer;
}
}

View File

@ -22,15 +22,15 @@ import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey;
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 {

Some files were not shown because too many files have changed in this diff Show More