[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.homie/ @ccutrer
/bundles/org.openhab.binding.mqtt.ruuvigateway/ @ssalonen /bundles/org.openhab.binding.mqtt.ruuvigateway/ @ssalonen
/bundles/org.openhab.binding.mycroft/ @dalgwen /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.mynice/ @clinique
/bundles/org.openhab.binding.mystrom/ @pail23 /bundles/org.openhab.binding.mystrom/ @pail23
/bundles/org.openhab.binding.nanoleaf/ @stefan-hoehn /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 | | 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 | | 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 Status | charge | String | Current charging status | | X | X | X |
| Charging Information | charge-info | String | Information regarding current charging session | | X | X | X | | Remaining Charging Time | charge-remaining | Number:Time | Remaining time for current charging session | | X | X | X |
| Motion Status | motion | Switch | Driving state - depends on vehicle hardware | X | X | X | X |
| Last Status Timestamp | last-update | DateTime | Date and time of last status update | X | X | X | X | | 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 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 - Availability according to table
- Read-only values - Read-only values
| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev | | Channel Label | Channel ID | Type | conv | phev | bev_rex | bev |
|---------------------------|-------------------------|----------------------|------|------|---------|-----| |------------------------------------|----------------------------|----------------------|------|------|---------|-----|
| Mileage | mileage | Number:Length | X | X | X | X | | Mileage | mileage | Number:Length | X | X | X | X |
| Fuel Range | range-fuel | Number:Length | X | X | X | | | Fuel Range | range-fuel | Number:Length | X | X | X | |
| Electric Range | range-electric | Number:Length | | X | X | X | | Electric Range | range-electric | Number:Length | | X | X | X |
| Hybrid Range | range-hybrid | Number:Length | | X | X | | | Hybrid Range | range-hybrid | Number:Length | | X | X | |
| Battery Charge Level | soc | Number:Dimensionless | | X | X | X | | Battery Charge Level | soc | Number:Dimensionless | | X | X | X |
| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | | | Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | |
| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | | | Estimated Fuel Consumption l/100km | estimated-fuel-l-100km | Number | X | X | X | |
| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X | | Estimated Fuel Consumption mpg | estimated-fuel-mpg | Number | X | X | X | |
| Hybrid Range Radius | range-radius-hybrid | Number:Length | | 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 #### Doors Details
@ -359,6 +361,7 @@ The channel _command_ provides options
- _horn-blow_ - _horn-blow_
- _climate-now-start_ - _climate-now-start_
- _climate-now-stop_ - _climate-now-stop_
- _charge-now_
The channel _state_ shows the progress of the command execution in the following order 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: Possible view ports:
- _VehicleStatus_ Front Side View - _VehicleStatus_ Front Left Side View
- _VehicleInfo_ Front View - _FrontView_ Front View
- _ChargingHistory_ Side View - _FrontLeft_ Front Left Side View
- _Default_ Front Side View - _FrontRight_ Front Right Side View
- _RearView_ Rear View
## Further Descriptions ## Further Descriptions
@ -491,7 +495,8 @@ There are 3 occurrences of dynamic data delivered
The channel id _name_ shows the first element as default. The channel id _name_ shows the first element as default.
All other possibilities are attached as options. All other possibilities are attached as options.
The picture on the right shows the _Session Title_ item and 3 possible 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 ### TroubleShooting
@ -507,32 +512,34 @@ If these preconditions are fulfilled proceed with the fingerprint generation.
#### Generate Debug Fingerprint #### 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 You can restrict the accounts and vehicles for the fingerprint generation.
log:set DEBUG org.openhab.binding.mybmw Full syntax is available through the `mybmw help` console command.
```
The debug fingerprint is generated every time the discovery is executed. Personal data is eliminated from fingerprints so it should be possible to share them in public.
To force a new fingerprint perform a _Scan_ for MyBMW things.
Personal data is eliminated from the log entries so it should be possible to share them in public.
Data like Data like
- Vehicle Identification Number (VIN) - Vehicle Identification Number (VIN)
- Location data - Location data
are anonymized. are anonymized in the JSON response and URL's.
You'll find the fingerprint in the logs with the command
```shell 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!
grep "Discovery Fingerprint Data" openhab.log
```
After the corresponding fingerprint is generated please [follow the instructions to raise an issue](https://community.openhab.org/t/how-to-file-an-issue/68464) and attach the fingerprint data!
Your feedback is highly appreciated! 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 ### Range vs Range Radius
<img align="right" src="./doc/range-radius.png" width="400" height="350"/> <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> <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> </project>

View File

@ -16,12 +16,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mybmw.internal.utils.Constants; 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 Bernd Weymann - Initial contribution
* @author Martin Grassl - renamed
*/ */
@NonNullByDefault @NonNullByDefault
public class MyBMWConfiguration { public class MyBMWBridgeConfiguration {
/** /**
* Depending on the location the correct server needs to be called * 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 Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit and send of charge profile * @author Norbert Truchsess - edit and send of charge profile
* @author Martin Grassl - updated enum values
*/ */
@NonNullByDefault @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; static final String REFRESH_INTERVAL = "refreshInterval";
public static final int DEFAULT_REFRESH_INTERVAL_MINUTES = 5;
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 // See constants from bimmer-connected
// https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/vehicle.py // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/vehicle.py
public enum VehicleType { enum VehicleType {
CONVENTIONAL("conv"), CONVENTIONAL("conv"),
PLUGIN_HYBRID("phev"), PLUGIN_HYBRID("phev"),
MILD_HYBRID("hybrid"), MILD_HYBRID("hybrid"),
@ -56,150 +82,150 @@ public class MyBMWConstants {
} }
} }
public enum ChargingMode { enum ChargingMode {
immediateCharging, IMMEDIATE_CHARGING,
delayedCharging DELAYED_CHARGING
} }
public enum ChargingPreference { enum ChargingPreference {
noPreSelection, NO_PRESELECTION,
chargingWindow 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()); 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()); VehicleType.PLUGIN_HYBRID.toString(), VehicleType.ELECTRIC_REX.toString());
// List of all Thing Type UIDs // List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CONNECTED_DRIVE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); static final ThingTypeUID THING_TYPE_CONNECTED_DRIVE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_CONV = new ThingTypeUID(BINDING_ID, static final ThingTypeUID THING_TYPE_CONV = new ThingTypeUID(BINDING_ID, VehicleType.CONVENTIONAL.toString());
VehicleType.CONVENTIONAL.toString()); static final ThingTypeUID THING_TYPE_PHEV = new ThingTypeUID(BINDING_ID, VehicleType.PLUGIN_HYBRID.toString());
public static final ThingTypeUID THING_TYPE_PHEV = new ThingTypeUID(BINDING_ID, static final ThingTypeUID THING_TYPE_BEV_REX = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC_REX.toString());
VehicleType.PLUGIN_HYBRID.toString()); static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC.toString());
public static final ThingTypeUID THING_TYPE_BEV_REX = new ThingTypeUID(BINDING_ID, static final Set<ThingTypeUID> SUPPORTED_THING_SET = Set.of(THING_TYPE_CONNECTED_DRIVE_ACCOUNT, THING_TYPE_CONV,
VehicleType.ELECTRIC_REX.toString()); THING_TYPE_PHEV, THING_TYPE_BEV_REX, THING_TYPE_BEV);
public static final ThingTypeUID THING_TYPE_BEV = new ThingTypeUID(BINDING_ID, VehicleType.ELECTRIC.toString());
public static final Set<ThingTypeUID> SUPPORTED_THING_SET = Set.of(THING_TYPE_CONNECTED_DRIVE_ACCOUNT,
THING_TYPE_CONV, THING_TYPE_PHEV, THING_TYPE_BEV_REX, THING_TYPE_BEV);
// Thing Group definitions // Thing Group definitions
public static final String CHANNEL_GROUP_STATUS = "status"; static final String CHANNEL_GROUP_STATUS = "status";
public static final String CHANNEL_GROUP_SERVICE = "service"; static final String CHANNEL_GROUP_SERVICE = "service";
public static final String CHANNEL_GROUP_CHECK_CONTROL = "check"; static final String CHANNEL_GROUP_CHECK_CONTROL = "check";
public static final String CHANNEL_GROUP_DOORS = "doors"; static final String CHANNEL_GROUP_DOORS = "doors";
public static final String CHANNEL_GROUP_RANGE = "range"; static final String CHANNEL_GROUP_RANGE = "range";
public static final String CHANNEL_GROUP_LOCATION = "location"; static final String CHANNEL_GROUP_LOCATION = "location";
public static final String CHANNEL_GROUP_REMOTE = "remote"; static final String CHANNEL_GROUP_REMOTE = "remote";
public static final String CHANNEL_GROUP_CHARGE_PROFILE = "profile"; static final String CHANNEL_GROUP_CHARGE_PROFILE = "profile";
public static final String CHANNEL_GROUP_CHARGE_STATISTICS = "statistic"; static final String CHANNEL_GROUP_CHARGE_STATISTICS = "statistic";
public static final String CHANNEL_GROUP_CHARGE_SESSION = "session"; static final String CHANNEL_GROUP_CHARGE_SESSION = "session";
public static final String CHANNEL_GROUP_TIRES = "tires"; static final String CHANNEL_GROUP_TIRES = "tires";
public static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image"; static final String CHANNEL_GROUP_VEHICLE_IMAGE = "image";
// Charge Statistics & Sessions // Charge Statistics & Sessions
public static final String SESSIONS = "sessions"; static final String SESSIONS = "sessions";
public static final String ENERGY = "energy"; static final String ENERGY = "energy";
public static final String TITLE = "title"; static final String TITLE = "title";
public static final String SUBTITLE = "subtitle"; static final String SUBTITLE = "subtitle";
public static final String ISSUE = "issue"; static final String ISSUE = "issue";
public static final String STATUS = "status"; static final String STATUS = "status";
// Generic Constants for several groups // Generic Constants for several groups
public static final String NAME = "name"; static final String NAME = "name";
public static final String DETAILS = "details"; static final String DETAILS = "details";
public static final String SEVERITY = "severity"; static final String SEVERITY = "severity";
public static final String DATE = "date"; static final String DATE = "date";
public static final String MILEAGE = "mileage"; static final String MILEAGE = "mileage";
public static final String GPS = "gps"; static final String GPS = "gps";
public static final String HEADING = "heading"; static final String HEADING = "heading";
public static final String ADDRESS = "address"; static final String ADDRESS = "address";
public static final String HOME_DISTANCE = "home-distance"; static final String HOME_DISTANCE = "home-distance";
// Status // Status
public static final String DOORS = "doors"; static final String DOORS = "doors";
public static final String WINDOWS = "windows"; static final String WINDOWS = "windows";
public static final String LOCK = "lock"; static final String LOCK = "lock";
public static final String SERVICE_DATE = "service-date"; static final String SERVICE_DATE = "service-date";
public static final String SERVICE_MILEAGE = "service-mileage"; static final String SERVICE_MILEAGE = "service-mileage";
public static final String CHECK_CONTROL = "check-control"; static final String CHECK_CONTROL = "check-control";
public static final String PLUG_CONNECTION = "plug-connection"; static final String PLUG_CONNECTION = "plug-connection";
public static final String CHARGE_STATUS = "charge"; static final String CHARGE_STATUS = "charge";
public static final String CHARGE_INFO = "charge-info"; static final String CHARGE_REMAINING = "charge-remaining";
public static final String MOTION = "motion"; static final String LAST_UPDATE = "last-update";
public static final String LAST_UPDATE = "last-update"; static final String LAST_FETCHED = "last-fetched";
public static final String RAW = "raw"; static final String RAW = "raw";
// Door Details // Door Details
public static final String DOOR_DRIVER_FRONT = "driver-front"; static final String DOOR_DRIVER_FRONT = "driver-front";
public static final String DOOR_DRIVER_REAR = "driver-rear"; static final String DOOR_DRIVER_REAR = "driver-rear";
public static final String DOOR_PASSENGER_FRONT = "passenger-front"; static final String DOOR_PASSENGER_FRONT = "passenger-front";
public static final String DOOR_PASSENGER_REAR = "passenger-rear"; static final String DOOR_PASSENGER_REAR = "passenger-rear";
public static final String HOOD = "hood"; static final String HOOD = "hood";
public static final String TRUNK = "trunk"; static final String TRUNK = "trunk";
public static final String WINDOW_DOOR_DRIVER_FRONT = "win-driver-front"; static final String WINDOW_DOOR_DRIVER_FRONT = "win-driver-front";
public static final String WINDOW_DOOR_DRIVER_REAR = "win-driver-rear"; static final String WINDOW_DOOR_DRIVER_REAR = "win-driver-rear";
public static final String WINDOW_DOOR_PASSENGER_FRONT = "win-passenger-front"; static final String WINDOW_DOOR_PASSENGER_FRONT = "win-passenger-front";
public static final String WINDOW_DOOR_PASSENGER_REAR = "win-passenger-rear"; static final String WINDOW_DOOR_PASSENGER_REAR = "win-passenger-rear";
public static final String WINDOW_REAR = "win-rear"; static final String WINDOW_REAR = "win-rear";
public static final String SUNROOF = "sunroof"; static final String SUNROOF = "sunroof";
// Charge Profile // Charge Profile
public static final String CHARGE_PROFILE_CLIMATE = "climate"; static final String CHARGE_PROFILE_CLIMATE = "climate";
public static final String CHARGE_PROFILE_MODE = "mode"; static final String CHARGE_PROFILE_MODE = "mode";
public static final String CHARGE_PROFILE_PREFERENCE = "prefs"; static final String CHARGE_PROFILE_PREFERENCE = "prefs";
public static final String CHARGE_PROFILE_CONTROL = "control"; static final String CHARGE_PROFILE_CONTROL = "control";
public static final String CHARGE_PROFILE_TARGET = "target"; static final String CHARGE_PROFILE_TARGET = "target";
public static final String CHARGE_PROFILE_LIMIT = "limit"; static final String CHARGE_PROFILE_LIMIT = "limit";
public static final String CHARGE_WINDOW_START = "window-start"; static final String CHARGE_WINDOW_START = "window-start";
public static final String CHARGE_WINDOW_END = "window-end"; static final String CHARGE_WINDOW_END = "window-end";
public static final String CHARGE_TIMER1 = "timer1"; static final String CHARGE_TIMER1 = "timer1";
public static final String CHARGE_TIMER2 = "timer2"; static final String CHARGE_TIMER2 = "timer2";
public static final String CHARGE_TIMER3 = "timer3"; static final String CHARGE_TIMER3 = "timer3";
public static final String CHARGE_TIMER4 = "timer4"; static final String CHARGE_TIMER4 = "timer4";
public static final String CHARGE_DEPARTURE = "-departure"; static final String CHARGE_DEPARTURE = "-departure";
public static final String CHARGE_ENABLED = "-enabled"; static final String CHARGE_ENABLED = "-enabled";
public static final String CHARGE_DAY_MON = "-day-mon"; static final String CHARGE_DAY_MON = "-day-mon";
public static final String CHARGE_DAY_TUE = "-day-tue"; static final String CHARGE_DAY_TUE = "-day-tue";
public static final String CHARGE_DAY_WED = "-day-wed"; static final String CHARGE_DAY_WED = "-day-wed";
public static final String CHARGE_DAY_THU = "-day-thu"; static final String CHARGE_DAY_THU = "-day-thu";
public static final String CHARGE_DAY_FRI = "-day-fri"; static final String CHARGE_DAY_FRI = "-day-fri";
public static final String CHARGE_DAY_SAT = "-day-sat"; static final String CHARGE_DAY_SAT = "-day-sat";
public static final String CHARGE_DAY_SUN = "-day-sun"; static final String CHARGE_DAY_SUN = "-day-sun";
// Range // Range
public static final String RANGE_ELECTRIC = "electric"; static final String RANGE_ELECTRIC = "electric";
public static final String RANGE_RADIUS_ELECTRIC = "radius-electric"; static final String RANGE_RADIUS_ELECTRIC = "radius-electric";
public static final String RANGE_FUEL = "fuel"; static final String RANGE_FUEL = "fuel";
public static final String RANGE_RADIUS_FUEL = "radius-fuel"; static final String RANGE_RADIUS_FUEL = "radius-fuel";
public static final String RANGE_HYBRID = "hybrid"; static final String RANGE_HYBRID = "hybrid";
public static final String RANGE_RADIUS_HYBRID = "radius-hybrid"; static final String RANGE_RADIUS_HYBRID = "radius-hybrid";
public static final String REMAINING_FUEL = "remaining-fuel"; static final String REMAINING_FUEL = "remaining-fuel";
public static final String SOC = "soc"; 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 // Image
public static final String IMAGE_FORMAT = "png"; static final String IMAGE_FORMAT = "png";
public static final String IMAGE_VIEWPORT = "view"; static final String IMAGE_VIEWPORT = "view";
// Remote Services // Remote Services
public static final String REMOTE_SERVICE_LIGHT_FLASH = "light-flash"; static final String REMOTE_SERVICE_LIGHT_FLASH = "light-flash";
public static final String REMOTE_SERVICE_VEHICLE_FINDER = "vehicle-finder"; static final String REMOTE_SERVICE_VEHICLE_FINDER = "vehicle-finder";
public static final String REMOTE_SERVICE_DOOR_LOCK = "door-lock"; static final String REMOTE_SERVICE_DOOR_LOCK = "door-lock";
public static final String REMOTE_SERVICE_DOOR_UNLOCK = "door-unlock"; static final String REMOTE_SERVICE_DOOR_UNLOCK = "door-unlock";
public static final String REMOTE_SERVICE_HORN = "horn-blow"; static final String REMOTE_SERVICE_HORN = "horn-blow";
public static final String REMOTE_SERVICE_AIR_CONDITIONING_START = "climate-now-start"; 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_AIR_CONDITIONING_STOP = "climate-now-stop";
static final String REMOTE_SERVICE_CHARGE = "charge-now";
public static final String REMOTE_SERVICE_COMMAND = "command"; static final String REMOTE_SERVICE_COMMAND = "command";
public static final String REMOTE_STATE = "state"; static final String REMOTE_STATE = "state";
// TIRES // TIRES
public static final String FRONT_LEFT_CURRENT = "fl-current"; static final String FRONT_LEFT_CURRENT = "fl-current";
public static final String FRONT_LEFT_TARGET = "fl-target"; static final String FRONT_LEFT_TARGET = "fl-target";
public static final String FRONT_RIGHT_CURRENT = "fr-current"; static final String FRONT_RIGHT_CURRENT = "fr-current";
public static final String FRONT_RIGHT_TARGET = "fr-target"; static final String FRONT_RIGHT_TARGET = "fr-target";
public static final String REAR_LEFT_CURRENT = "rl-current"; static final String REAR_LEFT_CURRENT = "rl-current";
public static final String REAR_LEFT_TARGET = "rl-target"; static final String REAR_LEFT_TARGET = "rl-target";
public static final String REAR_RIGHT_CURRENT = "rr-current"; static final String REAR_RIGHT_CURRENT = "rr-current";
public static final String REAR_RIGHT_TARGET = "rr-target"; static final String REAR_RIGHT_TARGET = "rr-target";
} }

View File

@ -12,7 +12,8 @@
*/ */
package org.openhab.binding.mybmw.internal; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.binding.mybmw.internal.handler.VehicleHandler;
import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.LocationProvider; import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
@ -37,6 +39,7 @@ import org.osgi.service.component.annotations.Reference;
* handlers. * handlers.
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
* @author Martin Grassl - changed localeProvider handling
*/ */
@NonNullByDefault @NonNullByDefault
@Component(configurationPid = "binding.mybmw", service = ThingHandlerFactory.class) @Component(configurationPid = "binding.mybmw", service = ThingHandlerFactory.class)
@ -44,15 +47,19 @@ public class MyBMWHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory; private final HttpClientFactory httpClientFactory;
private final MyBMWCommandOptionProvider commandOptionProvider; private final MyBMWCommandOptionProvider commandOptionProvider;
private final LocationProvider locationProvider; private final LocationProvider locationProvider;
private String localeLanguage; private final TimeZoneProvider timeZoneProvider;
private final LocaleProvider localeProvider;
@Activate @Activate
public MyBMWHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference MyBMWCommandOptionProvider cop, public MyBMWHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference LocaleProvider localeP, final @Reference LocationProvider locationP) { final @Reference MyBMWCommandOptionProvider commandOptionProvider,
httpClientFactory = hcf; final @Reference LocaleProvider localeProvider, final @Reference LocationProvider locationProvider,
commandOptionProvider = cop; final @Reference TimeZoneProvider timeZoneProvider) {
locationProvider = locationP; this.httpClientFactory = httpClientFactory;
localeLanguage = localeP.getLocale().getLanguage().toLowerCase(); this.commandOptionProvider = commandOptionProvider;
this.locationProvider = locationProvider;
this.timeZoneProvider = timeZoneProvider;
this.localeProvider = localeProvider;
} }
@Override @Override
@ -64,9 +71,10 @@ public class MyBMWHandlerFactory extends BaseThingHandlerFactory {
protected @Nullable ThingHandler createHandler(Thing thing) { protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_CONNECTED_DRIVE_ACCOUNT.equals(thingTypeUID)) { 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)) { } 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; 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; 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mybmw.internal.MyBMWConstants; import org.openhab.binding.mybmw.internal.MyBMWConstants;
import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; 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.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.Constants;
import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils; import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder; 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.ThingUID;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.thing.binding.ThingHandlerService;
@ -40,130 +41,34 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 Bernd Weymann - Initial contribution
* @author Martin Grassl - refactoring
*/ */
@NonNullByDefault @NonNullByDefault
public class VehicleDiscovery extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService { public class VehicleDiscovery extends AbstractDiscoveryService implements ThingHandlerService {
private static final Logger LOGGER = LoggerFactory.getLogger(VehicleDiscovery.class);
public static final String SUPPORTED_SUFFIX = "Supported"; private final Logger logger = LoggerFactory.getLogger(VehicleDiscovery.class);
public static final String ENABLE_SUFFIX = "Enable";
public static final String ENABLED_SUFFIX = "Enabled";
private static final int DISCOVERY_TIMEOUT = 10; private static final int DISCOVERY_TIMEOUT = 10;
private Optional<MyBMWBridgeHandler> bridgeHandler = Optional.empty(); private Optional<MyBMWBridgeHandler> bridgeHandler = Optional.empty();
private Optional<MyBMWProxy> myBMWProxy = Optional.empty();
private Optional<ThingUID> bridgeUid = Optional.empty();
public VehicleDiscovery() { public VehicleDiscovery() {
super(SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false); super(MyBMWConstants.SUPPORTED_THING_SET, DISCOVERY_TIMEOUT, false);
}
public void onResponse(List<Vehicle> vehicleList) {
bridgeHandler.ifPresent(bridge -> {
final ThingUID bridgeUID = bridge.getThing().getUID();
vehicleList.forEach(vehicle -> {
// the DriveTrain field in the delivered json is defining the Vehicle Type
String vehicleType = VehicleStatusUtils.vehicleType(vehicle.driveTrain, vehicle.model).toString();
SUPPORTED_THING_SET.forEach(entry -> {
if (entry.getId().equals(vehicleType)) {
ThingUID uid = new ThingUID(entry, vehicle.vin, bridgeUID.getId());
Map<String, String> properties = new HashMap<>();
// Vehicle Properties
properties.put("vehicleModel", vehicle.model);
properties.put("vehicleDriveTrain", vehicle.driveTrain);
properties.put("vehicleConstructionYear", Integer.toString(vehicle.year));
properties.put("vehicleBodytype", vehicle.bodyType);
properties.put("servicesSupported", getServices(vehicle, SUPPORTED_SUFFIX, true));
properties.put("servicesUnsupported", getServices(vehicle, SUPPORTED_SUFFIX, false));
String servicesEnabled = getServices(vehicle, ENABLED_SUFFIX, true) + Constants.SEMICOLON
+ getServices(vehicle, ENABLE_SUFFIX, true);
properties.put("servicesEnabled", servicesEnabled.trim());
String servicesDisabled = getServices(vehicle, ENABLED_SUFFIX, false) + Constants.SEMICOLON
+ getServices(vehicle, ENABLE_SUFFIX, false);
properties.put("servicesDisabled", servicesDisabled.trim());
// For RemoteServices we need to do it step-by-step
StringBuffer remoteServicesEnabled = new StringBuffer();
StringBuffer remoteServicesDisabled = new StringBuffer();
if (vehicle.capabilities.lock.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.DOOR_LOCK.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.unlock.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.DOOR_UNLOCK.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.lights.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.LIGHT_FLASH.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.horn.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.HORN_BLOW.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.vehicleFinder.isEnabled) {
remoteServicesEnabled.append(
RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
} else {
remoteServicesDisabled.append(
RemoteServiceHandler.RemoteService.VEHICLE_FINDER.getLabel() + Constants.SEMICOLON);
}
if (vehicle.capabilities.climateNow.isEnabled) {
remoteServicesEnabled.append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
+ Constants.SEMICOLON);
} else {
remoteServicesDisabled
.append(RemoteServiceHandler.RemoteService.CLIMATE_NOW_START.getLabel()
+ Constants.SEMICOLON);
}
properties.put("remoteServicesEnabled", remoteServicesEnabled.toString().trim());
properties.put("remoteServicesDisabled", remoteServicesDisabled.toString().trim());
// Update Properties for already created Things
bridge.getThing().getThings().forEach(vehicleThing -> {
Configuration c = vehicleThing.getConfiguration();
if (c.containsKey(MyBMWConstants.VIN)) {
String thingVIN = c.get(MyBMWConstants.VIN).toString();
if (vehicle.vin.equals(thingVIN)) {
vehicleThing.setProperties(properties);
}
}
});
// Properties needed for functional Thing
properties.put(MyBMWConstants.VIN, vehicle.vin);
properties.put("vehicleBrand", vehicle.brand);
properties.put("refreshInterval",
Integer.toString(MyBMWConstants.DEFAULT_REFRESH_INTERVAL_MINUTES));
String vehicleLabel = vehicle.brand + " " + vehicle.model;
Map<String, Object> convertedProperties = new HashMap<String, Object>(properties);
thingDiscovered(DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
.withRepresentationProperty(MyBMWConstants.VIN).withLabel(vehicleLabel)
.withProperties(convertedProperties).build());
}
});
});
});
} }
@Override @Override
public void setThingHandler(ThingHandler handler) { public void setThingHandler(ThingHandler handler) {
if (handler instanceof MyBMWBridgeHandler bmwBridgeHandler) { if (handler instanceof MyBMWBridgeHandler bmwBridgeHandler) {
logger.trace("VehicleDiscovery.setThingHandler for MybmwBridge");
bridgeHandler = Optional.of(bmwBridgeHandler); 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 @Override
protected void startScan() { protected void startScan() {
bridgeHandler.ifPresent(MyBMWBridgeHandler::requestVehicles); logger.trace("VehicleDiscovery.startScan");
discoverVehicles();
} }
@Override @Override
public void deactivate() { public void deactivate() {
logger.trace("VehicleDiscovery.deactivate");
super.deactivate(); super.deactivate();
} }
public static String getServices(Vehicle vehicle, String suffix, boolean enabled) { public void discoverVehicles() {
StringBuffer sb = new StringBuffer(); logger.trace("VehicleDiscovery.discoverVehicles");
List<String> l = getObject(vehicle.capabilities, enabled);
for (String capEntry : l) { myBMWProxy = bridgeHandler.get().getMyBmwProxy();
// remove "is" prefix
String cut = capEntry.substring(2); try {
if (cut.endsWith(suffix)) { Optional<List<@NonNull Vehicle>> vehicleList = myBMWProxy.map(prox -> {
if (sb.length() > 0) { try {
sb.append(Constants.SEMICOLON); 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 * this method is called by the bridgeHandler if the list of vehicles was retrieved successfully
* Used to get e.g. all services which are "ACTIVATED" *
* * it iterates through the list of existing things and checks if the vehicles found via the API
* @param dto Object * call are already known to OH. If not, it creates a new thing and puts it into the inbox
* @param compare String which needs to map with the value *
* @return String with all field names matching this value separated with Spaces * @param vehicleList
*/ */
public static List<String> getObject(Object dto, Object compare) { private void processVehicles(List<Vehicle> vehicleList) {
List<String> l = new ArrayList<String>(); logger.trace("VehicleDiscovery.processVehicles");
for (Field field : dto.getClass().getDeclaredFields()) {
try { vehicleList.forEach(vehicle -> {
Object value = field.get(dto); // the DriveTrain field in the delivered json is defining the Vehicle Type
if (compare.equals(value)) { String vehicleType = VehicleStatusUtils
l.add(field.getName()); .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 * The {@link AuthQueryResponse} Data Transfer Object
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
* @author Martin Grassl - add toString for debugging
*/ */
public class AuthQueryResponse { public class AuthQueryResponse {
public String clientName;// ": "mybmwapp", public String clientName;// ": "mybmwapp",
@ -47,4 +48,17 @@ public class AuthQueryResponse {
// "authenticate_user" // "authenticate_user"
// ], // ],
public List<String> promptValues; // ": ["login"] 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; 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 * @author Bernd Weymann - Initial contribution
*/ */
public class ChargeSessionsContainer { public class ChargingSessionsContainer {
public Object paginationInfo; 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 * The {@link ChargingSettings} Data Transfer Object
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
* @author Martin Grassl - refactored to Java Bean
*/ */
public class ChargingSettings { public class ChargingSettings {
public int targetSoc;// ": 100, private int acCurrentLimit = -1; // 32,
public boolean isAcCurrentLimitActive;// ": false, private String hospitality = ""; // HOSP_INACTIVE,
public String hospitality;// ": "NO_ACTION", private String idcc = ""; // AUTOMATIC_INTELLIGENT,
public String idcc;// ": "NO_ACTION" 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 * The {@link ChargingWindow} Data Transfer Object
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
* @author Martin Grassl - refactored to Java Bean
*/ */
public class ChargingWindow { public class ChargingWindow {
public Time start; private Time start = new Time();
public Time end; 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; package org.openhab.binding.mybmw.internal.dto.charge;
import org.openhab.binding.mybmw.internal.utils.Converter;
/** /**
* The {@link Time} Data Transfer Object * The {@link Time} Data Transfer Object
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
* @author Norbert Truchsess - edit and send of charge profile * @author Norbert Truchsess - edit and send of charge profile
* @author Martin Grassl - refactored to Java Bean
*/ */
public class Time { public class Time {
public int hour;// ": 11, private int hour = -1;// ": 11,
public int minute;// ": 0 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 @Override
public String toString() { 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 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 // Sicherheitsgründen sind Remote Services nicht verfügbar, wenn die Fahrbereitschaft
// eingeschaltet ist. Remote Services können nur mit einem ausreichenden Ladezustand // 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.", // ausgeführt werden, wenn die Fahrertür geschlossen und der Türstatus bekannt ist.",
public String presentationType;// ": "PAGE", public String presentationType;// ": "PAGE",
public int iconId;// ": 60217, public int iconId;// ": 60217,

View File

@ -16,10 +16,49 @@ package org.openhab.binding.mybmw.internal.dto.remote;
* The {@link ExecutionStatusContainer} Data Transfer Object * The {@link ExecutionStatusContainer} Data Transfer Object
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
* @author Martin Grassl - refactored to Java Bean
*/ */
public class ExecutionStatusContainer { public class ExecutionStatusContainer {
public String eventId; private String eventId = "";
public String creationTime; private String creationTime = "";
public String eventStatus; private String eventStatus = "";
public ExecutionError errorDetails; 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 * 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 * The {@link Address} Data Transfer Object
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
* @author Martin Grassl - refactored to Java Bean
*/ */
public class Address { 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; package org.openhab.binding.mybmw.internal.dto.vehicle;
/** /**
* The {@link RemoteService} Data Transfer Object *
* * derived from the API responses
* @author Bernd Weymann - Initial contribution *
* @author Martin Grassl - initial contribution
*/ */
public class RemoteService { public class ClimateControlState {
public boolean isEnabled;// ": true, private String activity = ""; // INACTIVE
public boolean isPinAuthenticationRequired;// ": false,
public String executionMessage;// ": "Lock your vehicle now? Remote functions may take a few seconds." 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; 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 * The {@link Vehicle} Data Transfer Object
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
* @author Martin Grassl - refactored for v2 API
*/ */
public class Vehicle { public class Vehicle {
public String vin;// ": "WBY1Z81040V905639", private VehicleBase vehicleBase = new VehicleBase();
public String model;// ": "i3 94 (+ REX)", private VehicleStateContainer vehicleState = new VehicleStateContainer();
public int year;// ": 2017,
public String brand;// ": "BMW", public VehicleBase getVehicleBase() {
public String headUnit;// ": "ID5", return vehicleBase;
public boolean isLscSupported;// ": true, }
public String driveTrain;// ": "ELECTRIC",
public String puStep;// ": "0321", public void setVehicleBase(VehicleBase vehicleBase) {
public String iStep;// ": "I001-21-03-530", this.vehicleBase = vehicleBase;
public String telematicsUnit;// ": "TCB1", }
public String hmiVersion;// ": "ID4",
public String bodyType;// ": "I01", public VehicleStateContainer getVehicleState() {
public String a4aType;// ": "USB_ONLY", return vehicleState;
public String exFactoryPUStep;// ": "0717", }
public String exFactoryILevel;// ": "I001-17-07-500"
public Capabilities capabilities; public void setVehicleState(VehicleStateContainer vehicleState) {
// "connectedDriveServices": [] currently no clue how to resolve, this.vehicleState = vehicleState;
public Properties properties; }
public boolean isMappingPending;// ":false,"
public boolean isMappingUnconfirmed;// ":false, @Override
public Status status; public String toString() {
public boolean valid = false; 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.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration;
import org.openhab.binding.mybmw.internal.MyBMWConfiguration;
import org.openhab.binding.mybmw.internal.discovery.VehicleDiscovery; import org.openhab.binding.mybmw.internal.discovery.VehicleDiscovery;
import org.openhab.binding.mybmw.internal.dto.network.NetworkError; import org.openhab.binding.mybmw.internal.handler.backend.MyBMWFileProxy;
import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle; import org.openhab.binding.mybmw.internal.handler.backend.MyBMWHttpProxy;
import org.openhab.binding.mybmw.internal.utils.BimmerConstants; import org.openhab.binding.mybmw.internal.handler.backend.MyBMWProxy;
import org.openhab.binding.mybmw.internal.utils.Constants; 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.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
@ -40,102 +39,118 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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. * sent to one of the channels.
* *
* @author Bernd Weymann - Initial contribution * @author Bernd Weymann - Initial contribution
* @author Martin Grassl - refactored, all discovery functionality moved to VehicleDiscovery
*/ */
@NonNullByDefault @NonNullByDefault
public class MyBMWBridgeHandler extends BaseBridgeHandler implements StringResponseCallback { public class MyBMWBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(MyBMWBridgeHandler.class);
private HttpClientFactory httpClientFactory;
private Optional<VehicleDiscovery> discoveryService = Optional.empty();
private Optional<MyBMWProxy> proxy = Optional.empty();
private Optional<ScheduledFuture<?>> initializerJob = Optional.empty();
private Optional<String> troubleshootFingerprint = Optional.empty();
private String localeLanguage;
public MyBMWBridgeHandler(Bridge bridge, HttpClientFactory hcf, String language) { 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); super(bridge);
httpClientFactory = hcf; httpClientFactory = hcf;
localeLanguage = language; this.localeProvider = localeProvider;
}
public void setVehicleDiscovery(VehicleDiscovery vehicleDiscovery) {
logger.trace("MyBMWBridgeHandler.setVehicleDiscovery");
this.vehicleDiscovery = Optional.of(vehicleDiscovery);
} }
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
// no commands available // no commands available
logger.trace("MyBMWBridgeHandler.handleCommand");
} }
@Override @Override
public void initialize() { public void initialize() {
troubleshootFingerprint = Optional.empty(); logger.trace("MyBMWBridgeHandler.initialize");
updateStatus(ThingStatus.UNKNOWN); updateStatus(ThingStatus.UNKNOWN);
MyBMWConfiguration config = getConfigAs(MyBMWConfiguration.class); MyBMWBridgeConfiguration config = getConfigAs(MyBMWBridgeConfiguration.class);
if (config.language.equals(Constants.LANGUAGE_AUTODETECT)) { 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); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
} else { } else {
proxy = Optional.of(new MyBMWProxy(httpClientFactory, config)); // there is no risk in this functionality as several steps have to happen to get the file proxy working:
initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 2, TimeUnit.SECONDS)); // 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) { private synchronized void createMyBmwProxy(MyBMWBridgeConfiguration config, String environment) {
if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) { if (!myBmwProxy.isPresent()) {
return false; if (!(TEST.equals(environment) && TESTUSER.equals(config.userName))) {
} else { myBmwProxy = Optional.of(new MyBMWHttpProxy(httpClientFactory, config));
return BimmerConstants.EADRAX_SERVER_MAP.containsKey(config.region); } else {
myBmwProxy = Optional.of(new MyBMWFileProxy(httpClientFactory, config));
}
logger.trace("MyBMWBridgeHandler proxy set");
} }
} }
@Override @Override
public void dispose() { public void dispose() {
logger.trace("MyBMWBridgeHandler.dispose");
initializerJob.ifPresent(job -> job.cancel(true)); initializerJob.ifPresent(job -> job.cancel(true));
} }
public void requestVehicles() { public void vehicleDiscoveryError() {
proxy.ifPresent(prox -> prox.requestVehicles(this)); logger.trace("MyBMWBridgeHandler.vehicleDiscoveryError");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Request vehicles failed");
} }
private void logFingerPrint() { public void vehicleDiscoverySuccess() {
logger.debug("###### Discovery Fingerprint Data - BEGIN ######"); logger.trace("MyBMWBridgeHandler.vehicleDiscoverySuccess");
logger.debug("{}", troubleshootFingerprint.get()); updateStatus(ThingStatus.ONLINE);
logger.debug("###### Discovery Fingerprint Data - END ######");
} }
/** private void discoverVehicles() {
* Response for vehicle request logger.trace("MyBMWBridgeHandler.requestVehicles");
*/
@Override
public synchronized void onResponse(@Nullable String response) {
if (response != null) {
updateStatus(ThingStatus.ONLINE);
List<Vehicle> vehicleList = Converter.getVehicleList(response);
discoveryService.get().onResponse(vehicleList);
troubleshootFingerprint = Optional.of(Converter.anonymousFingerprint(response));
logFingerPrint();
}
}
@Override MyBMWBridgeConfiguration config = getConfigAs(MyBMWBridgeConfiguration.class);
public void onError(NetworkError error) {
troubleshootFingerprint = Optional.of(error.toJson()); myBmwProxy.ifPresent(proxy -> proxy.setBridgeConfiguration(config));
logFingerPrint();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error.reason); vehicleDiscovery.ifPresent(discovery -> discovery.discoverVehicles());
} }
@Override @Override
public Collection<Class<? extends ThingHandlerService>> getServices() { public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(VehicleDiscovery.class); logger.trace("MyBMWBridgeHandler.getServices");
return List.of(VehicleDiscovery.class);
} }
public Optional<MyBMWProxy> getProxy() { public Optional<MyBMWProxy> getMyBmwProxy() {
return proxy; logger.trace("MyBMWBridgeHandler.getProxy");
} createMyBmwProxy(getConfigAs(MyBMWBridgeConfiguration.class), ENVIRONMENT);
return myBmwProxy;
public void setDiscoveryService(VehicleDiscovery discoveryService) {
this.discoveryService = Optional.of(discoveryService);
} }
} }

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 * 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.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mybmw.internal.utils.Constants; 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 * 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; import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link ByteResponseCallback} Interface for all raw byte results from ASYNC REST API *
* * execution state of a remote command
* @author Bernd Weymann - Initial contribution *
* @author Martin Grassl - initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public interface ByteResponseCallback extends ResponseCallback { public enum ExecutionState {
READY,
void onResponse(byte[] result); 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> * <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 Bernd Weymann - Initial contribution
* @author Martin Grassl - update to v2 API
*/ */
@NonNullByDefault @NonNullByDefault
public class BimmerConstants { public interface BimmerConstants {
public static final String REGION_NORTH_AMERICA = "NORTH_AMERICA"; static final String REGION_NORTH_AMERICA = "NORTH_AMERICA";
public static final String REGION_CHINA = "CHINA"; static final String REGION_CHINA = "CHINA";
public static final String REGION_ROW = "ROW"; static final String REGION_ROW = "ROW";
public static final String BRAND_BMW = "bmw"; static final String BRAND_BMW = "bmw";
public static final String BRAND_MINI = "mini"; static final String BRAND_BMWI = "bmw_i";
public static final List<String> ALL_BRANDS = List.of(BRAND_BMW, BRAND_MINI); 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"; static final String EADRAX_SERVER_NORTH_AMERICA = "cocoapi.bmwgroup.us";
public static final String EADRAX_SERVER_ROW = "cocoapi.bmwgroup.com"; static final String EADRAX_SERVER_ROW = "cocoapi.bmwgroup.com";
public static final String EADRAX_SERVER_CHINA = "myprofile.bmw.com.cn"; static final String EADRAX_SERVER_CHINA = "myprofile.bmw.com.cn";
public static final Map<String, String> EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, static final Map<String, String> EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, EADRAX_SERVER_NORTH_AMERICA,
EADRAX_SERVER_NORTH_AMERICA, REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW); REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW);
public static final String OCP_APIM_KEY_NORTH_AMERICA = "31e102f5-6f7e-7ef3-9044-ddce63891362"; 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"; 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 Map<String, String> OCP_APIM_KEYS = Map.of(REGION_NORTH_AMERICA, OCP_APIM_KEY_NORTH_AMERICA,
REGION_ROW, OCP_APIM_KEY_ROW); REGION_ROW, OCP_APIM_KEY_ROW);
public static final String CHINA_PUBLIC_KEY = "/eadrax-coas/v1/cop/publickey"; 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_LOGIN = "/eadrax-coas/v2/login/pwd";
// Http variables // Http variables
public static final String USER_AGENT = "Dart/2.14 (dart:io)"; static final String APP_VERSION_NORTH_AMERICA = "2.12.0(19883)";
public static final String X_USER_AGENT = "android(SP1A.210812.016.C1);%s;2.5.2(14945);%s"; 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"; static final String LOGIN_NONCE = "login_nonce";
public static final String AUTHORIZATION_CODE = "authorization_code"; static final String AUTHORIZATION_CODE = "authorization_code";
// Parameters for API Requests // Parameters for API Requests
public static final String TIRE_GUARD_MODE = "tireGuardMode"; static final String TIRE_GUARD_MODE = "tireGuardMode";
public static final String APP_DATE_TIME = "appDateTime"; static final String APP_DATE_TIME = "appDateTime";
public static final String APP_TIMEZONE = "apptimezone"; static final String APP_TIMEZONE = "apptimezone";
// API endpoints // API endpoints
public static final String API_OAUTH_CONFIG = "/eadrax-ucs/v1/presentation/oauth/config"; static final String API_OAUTH_CONFIG = "/eadrax-ucs/v1/presentation/oauth/config";
public static final String API_VEHICLES = "/eadrax-vcs/v1/vehicles"; static final String API_VEHICLES = "/eadrax-vcs/v4/vehicles";
public static final String API_REMOTE_SERVICE_BASE_URL = "/eadrax-vrccs/v2/presentation/remote-commands/"; // '/{vin}/{service_type}' static final String API_REMOTE_SERVICE_BASE_URL = "/eadrax-vrccs/v3/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_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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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 * @author Norbert Truchsess - initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class ChargeProfileUtils { public class ChargingProfileUtils {
// Charging // Charging
public static class TimedChannel { public static class TimedChannel {

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