added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.volvooncall</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,241 @@
# VolvoOnCall Binding
This binding integrates the [Volvo On Call](https://www.volvocars.com/intl/own/connectivity/volvo-on-call) compatible vehicles.
The integration happens through the WirelessCar Remote API.
## Supported Things
All cars compatible with Volvo On Call shall be supported by this binding.
## Discovery
Once a VocApi Bridge has been created with according credential, vehicles connected to this account will automatically be detected.
## Binding Configuration
The binding has no configuration options itself, all configuration is done at 'Things' level.
## Bridge Configuration
The 'VolvoOnCall API' bridge uses the owner's email address and password in order to access the VOC Remote API.
This is the same email address and password as used in the VolvoOnCall smartphone app, that allows to remotely control your car(s).
| Parameter | Description | Required |
|-----------|------------------------------------------------------------------------- |--------- |
| username | Username from the VolvoOnCall app (email address) | yes |
| password | Password from the VolvoOnCall app | yes |
Once the bridge created, you will be able to launch discovery of the vehicles attached to it.
## Thing Configuration
The 'VolvoOnCall API' bridge uses the owner's email address and password in order to access the VOC Remote API.
| Parameter | Name | Description | Required |
|-----------------|------------------|---------------------------------------------------------|----------|
| vin | Vin | Vehicle Identification Number of the car | yes |
| refreshinterval | Refresh interval | Interval in minutes to refresh the data (default=10) | no |
## Channels
All numeric channels use the [UoM feature](https://openhab.org/blog/2018/02/22/units-of-measurement.html).
This means you can easily change the desired unit e.g. miles/h instead of km/h just in your item definition.
#####Thing properties
Some of the channels are only available for specific cars and models.
Availability of specific action can be found in PaperUI -> Configuration -> Things -> <your car thing> -> SHOW PROPERTIES
Following channels are currently available:
| Channel Type ID | Item Type | Description | Remark |
|-----------------------------------------------|----------------------|--------------------------------------------------|--------------------------------------------------|
| doors#frontLeft | Contact | Door front left | |
| doors#frontRight | Contact | Door front right | |
| doors#rearLeft | Contact | Door rear left | |
| doors#rearRight | Contact | Door rear right | |
| doors#hood | Contact | Hood | |
| doors#tailgate | Contact | Tailgate | |
| doors#carLocked | Switch | Is the car locked | Can also be used to lock / unlock the car (see thing properties above) |
| windows#frontLeftWnd | Contact | Window front left | |
| windows#frontRightWnd | Contact | Window front right | |
| windows#rearLeftWnd | Contact | Window rear left | |
| windows#rearRightWnd | Contact | Window rear right | |
| odometer#odometer | Number:Length | Odometer value | |
| odometer#tripmeter1 | Number:Length | Trip meter 1 value | |
| odometer#tripmeter2 | Number:Length | Trip meter 2 value | |
| tank#fuelAmount | Number:Volume | Amount of fuel left in the tank | |
| tank#fuelLevel | Number:Dimensionless | Percentage of fuel left in the tank | |
| tank#fuelAlert | Switch | Alert if the amount of fuel is running low | ON when distancy to empty < 100 |
| tank#distanceToEmpty | Number:Length | Distance till tank is empty | |
| position#location | Location | Location of the car | |
| position#locationTimestamp | DateTime | Timestamp of the latest confirmed location | |
| tyrePressure#frontLeftTyre | String | Tyrepressure front left tyre | Normal / LowSoft |
| tyrePressure#frontRightTyr | String | Tyrepressure front right tyre | Normal / LowSoft |
| tyrePressure#rearLeftTyre | String | Tyrepressure rear left tyre | Normal / LowSoft |
| tyrePressure#rearRightTyre | String | Tyrepressure rear right tyre | Normal / LowSoft |
| other#averageSpeed | Number:Speed | Average speed | |
| other#engineRunning | Switch | Is the car engine running | |
| other#remoteHeater | Switch | Start the car remote heater | Only if property 'remoteHeater' is true (see thing properties above) |
| other#preclimatization | Switch | Start the car preclimatization | Only if property 'preclimatization' is true (see thing properties above) |
| other#brakeFluidLevel | String | Brake fluid level | Normal / Low / VeryLow |
| other#washerFluidLevel | String | Washer fluid level | Normal / Low / VeryLow |
| other#serviceWarning | String | Warning if service is needed | |
| other#bulbFailure | Switch | ON if at least one bulb is reported as failed | |
| battery#batteryLevel | Number:Dimensionless | Battery level | Only for Plugin hybrid / Twin Engine models |
| battery#batteryDistanceToEmpty | Number:Length | Distance until battery is empty | Only for Plugin hybrid / Twin Engine models |
| battery#chargeStatus | String | Charging status | Only for Plugin hybrid / Twin Engine models |
| battery#timeToHVBatteryFullyCharged | Number:Time | Time in minutes until the battery is fully charged| Only for Plugin hybrid / Twin Engine models |
| battery#chargingEnd | DateTime | Calculated time when the battery is fully charged| Only for Plugin hybrid / Twin Engine models |
| lasttrip#tripConsumption | Number:Volume | Last trip fuel consumption | |
| lasttrip#tripDistance | Number:Length | Last trip distance | |
| lasttrip#tripStartTime | DateTime | Last trip start time | |
| lasttrip#tripEndTime | DateTime | Last trip end time | |
| lasttrip#tripDuration | Number:Time | Last trip duration | |
| lasttrip#tripStartOdometer | Number:Length | Last trip start odometer | |
| lasttrip#tripStopOdometer | Number:Length | Last trip stop odometer | |
| lasttrip#startPosition | Location | Last trip start location | |
| lasttrip#endPosition | Location | Last trip end location | |
## Full Example
demo.things:
```
Bridge volvooncall:vocapi:glh "VoC Gaël" @ "System" [username="mail@address.org", password="mypassword"]
{
Thing vehicle XC60 "XC60" @ "World" [vin="theCarVIN", refreshinterval=5]
}
```
demo.items:
```
Group gVoc "Volvo On Call"
Group:Contact:OR(OPEN,CLOSED) gDoorsOpening "Portes" (gVoc)
Contact Voc_DoorsTailgate "Tailgate" (gDoorsOpening) {channel="volvooncall:vehicle:glh:XC60:doors#tailgate"}
Contact Voc_DoorsRearRight "Rear right" (gDoorsOpening) {channel="volvooncall:vehicle:glh:XC60:doors#rearRight"}
Contact Voc_DoorsRearLeft "Rear Left" (gDoorsOpening) {channel="volvooncall:vehicle:glh:XC60:doors#rearLeft"}
Contact Voc_DoorsFrontRight "Passager" (gDoorsOpening) {channel="volvooncall:vehicle:glh:XC60:doors#frontRight"}
Contact Voc_DoorsFrontLeft "Conducteur" (gDoorsOpening) {channel="volvooncall:vehicle:glh:XC60:doors#frontLeft"}
Contact Voc_DoorsHood "Hood" (gDoorsOpening) {channel="volvooncall:vehicle:glh:XC60:doors#hood"}
Group:Contact:OR(OPEN,CLOSED) gWindowsOpening "Fenêtres" (gVoc)
Contact Voc_WindowsRearRightWnd "Rear right" (gWindowsOpening) {channel="volvooncall:vehicle:glh:XC60:windows#rearRightWnd"}
Contact Voc_WindowsRearLeftWnd "Rear Left" (gWindowsOpening) {channel="volvooncall:vehicle:glh:XC60:windows#rearLeftWnd"}
Contact Voc_WindowsFrontRightWnd "Passager" (gWindowsOpening) {channel="volvooncall:vehicle:glh:XC60:windows#frontRightWnd"}
Contact Voc_WindowsFrontLeftWnd "Conducteur" (gWindowsOpening) {channel="volvooncall:vehicle:glh:XC60:windows#frontLeftWnd"}
Switch Voc_DoorsCarLocked "Verouillée" (gVoc) {channel="volvooncall:vehicle:glh:XC60:doors#carLocked"}
Number:Length Voc_Odometer "Kilométrage [%d %unit%]" (gVoc) {channel="volvooncall:vehicle:glh:XC60:odometer#odometer"}
Number:Dimensionless Voc_FuelLevel "Fuel Level" <sewerage> (gVoc) {channel="volvooncall:vehicle:glh:XC60:tank#fuelLevel"}
Switch Voc_Fuel_Alert "Niveau Carburant" <siren> (gVoc) {channel="volvooncall:vehicle:glh:XC60:tank#fuelAlert"}
String Voc_Fluid_Message "Lave Glace" (gVoc) {channel="volvooncall:vehicle:glh:XC60:other#washerFluidLevel"}
Location Voc_Location "Location" (gVoc) {channel="volvooncall:vehicle:glh:XC60:position#location"}
DateTime Voc_Location_LUD "Timestamp [%1$tH:%1$tM]" <time> (gVoc) {channel="volvooncall:vehicle:glh:XC60:position#locationTimestamp"}
Switch Voc_Fluid_Alert "Alerte Lave Glace" <siren> (gVoc)
```
voc.sitemap:
```
sitemap voc label="Volvo On Call" {
Frame label="Etat Véhicule" {
Switch item=Voc_DoorsCarLocked
Switch item=Voc_Location_LUD mappings=[REFRESH='MAJ !']
Default item=Voc_Odometer
Default item=Voc_FuelLevel
Default item=Voc_Fuel_Alert
Default item=Voc_Fluid_Message
Default item=Voc_Fluid_Alert
}
Frame label="" {
Mapview item=Voc_Location label="" height=10
}
Frame label="Opening Status" {
Group item=gDoorsOpening
Group item=gWindowsOpening
}
}
```
## Rule Actions
Multiple actions are supported by this binding. In classic rules these are accessible as shown in the example below:
Example 1a: If Thing has been created using autodiscovery
```
val actions = getActions("volvooncall","volvooncall:vehicle:thingId")
if(null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
} else {
actions.openCarCommand()
}
```
Example 1b: If Thing has been created using script
```
val actions = getActions("volvooncall","volvooncall:vehicle:bridgeId:thingId")
if(null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
} else {
actions.openCarCommand()
}
```
### closeCarCommand()
Sends the command to close the car.
### openCarCommand()
Sends the command to open the car.
### engineStartCommand(runtime)
Sends the command to start the engine for a given runtime. Default 5 minutes.
Parameters:
| Name | Description |
|---------|-----------------------------------------------|
| runtime | Integer - Time for the engine to stay on |
### heaterStartCommand()
Sends the command to start the car heater (if remoteHeaterSupported).
### heaterStopCommand()
Sends the command to stop the car heater (if remoteHeaterSupported).
### preclimatizationStartCommand()
Sends the command to start the car heater (if preclimatizationSupported).
### preclimatizationStopCommand()
Sends the command to stop the car heater (if preclimatizationSupported).
### honkBlinkCommand(honk, blink)
Activates lights and/or the horn of the car
Parameters:
| Name | Description |
|---------|-------------------------------------------|
| honk | Boolean - Activates the car horn |
| blink | Boolean - Activates the car lights |

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.volvooncall</artifactId>
<name>openHAB Add-ons :: Bundles :: Volvo On Call Binding</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.astro-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-volvooncall" description="Volvo On Call Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.volvooncall/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,128 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link VolvoOnCallBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallBindingConstants {
public static final String BINDING_ID = "volvooncall";
// Vehicle properties
public static final String VIN = "vin";
// The URL to use to connect to VocAPI with.
// TODO : for North America and China syntax changes to vocapi-cn.xxx
public static final String SERVICE_URL = "https://vocapi.wirelesscar.net/customerapi/rest/v3.0/";
// The JSON content type used when talking to VocAPI.
public static final String JSON_CONTENT_TYPE = "application/json";
// List of Thing Type UIDs
public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vocapi");
public static final ThingTypeUID VEHICLE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vehicle");
// List of Channel groups
public static final String GROUP_DOORS = "doors";
public static final String GROUP_WINDOWS = "windows";
public static final String GROUP_TYRES = "tyrePressure";
public static final String GROUP_BATTERY = "battery";
// List of Channel id's
public static final String TAILGATE = "tailgate";
public static final String REAR_RIGHT = "rearRight";
public static final String REAR_LEFT = "rearLeft";
public static final String FRONT_RIGHT = "frontRight";
public static final String FRONT_LEFT = "frontLeft";
public static final String HOOD = "hood";
public static final String REAR_RIGHT_WND = "rearRightWnd";
public static final String REAR_LEFT_WND = "rearLeftWnd";
public static final String FRONT_RIGHT_WND = "frontRightWnd";
public static final String FRONT_LEFT_WND = "frontLeftWnd";
public static final String REAR_RIGHT_TYRE = "rearRightTyre";
public static final String REAR_LEFT_TYRE = "rearLeftTyre";
public static final String FRONT_RIGHT_TYRE = "frontRightTyre";
public static final String FRONT_LEFT_TYRE = "frontLeftTyre";
public static final String ODOMETER = "odometer";
public static final String TRIPMETER1 = "tripmeter1";
public static final String TRIPMETER2 = "tripmeter2";
public static final String DISTANCE_TO_EMPTY = "distanceToEmpty";
public static final String FUEL_AMOUNT = "fuelAmount";
public static final String FUEL_LEVEL = "fuelLevel";
public static final String FUEL_CONSUMPTION = "fuelConsumption";
public static final String FUEL_ALERT = "fuelAlert";
public static final String CALCULATED_LOCATION = "calculatedLocation";
public static final String ACTUAL_LOCATION = "location";
public static final String LOCATION_TIMESTAMP = "locationTimestamp";
public static final String HEADING = "heading";
public static final String CAR_LOCKED = "carLocked";
public static final String ENGINE_RUNNING = "engineRunning";
public static final String BRAKE_FLUID_LEVEL = "brakeFluidLevel";
public static final String WASHER_FLUID_LEVEL = "washerFluidLevel";
public static final String AVERAGE_SPEED = "averageSpeed";
public static final String SERVICE_WARNING = "serviceWarningStatus";
public static final String BATTERY_LEVEL = "batteryLevel";
public static final String BATTERY_DISTANCE_TO_EMPTY = "batteryDistanceToEmpty";
public static final String CHARGE_STATUS = "chargeStatus";
public static final String TIME_TO_BATTERY_FULLY_CHARGED = "timeToHVBatteryFullyCharged";
public static final String CHARGING_END = "chargingEnd";
public static final String BULB_FAILURE = "bulbFailure";
// Last Trip Channel Id's
public static final String LAST_TRIP_GROUP = "lasttrip";
public static final String TRIP_CONSUMPTION = "tripConsumption";
public static final String TRIP_DISTANCE = "tripDistance";
public static final String TRIP_DURATION = "tripDuration";
public static final String TRIP_START_TIME = "tripStartTime";
public static final String TRIP_END_TIME = "tripEndTime";
public static final String TRIP_START_ODOMETER = "tripStartOdometer";
public static final String TRIP_STOP_ODOMETER = "tripStopOdometer";
public static final String TRIP_START_POSITION = "startPosition";
public static final String TRIP_END_POSITION = "endPosition";
// Optional Channels depends upon car version
public static final String CAR_LOCATOR = "carLocator";
public static final String JOURNAL_LOG = "journalLog";
// Car properties
public static final String ENGINE_START = "engineStart";
public static final String UNLOCK = "unlock";
public static final String UNLOCK_TIME = "unlockTimeFrame";
public static final String LOCK = "lock";
public static final String HONK = "honk";
public static final String BLINK = "blink";
public static final String HONK_BLINK = "honkAndBlink";
public static final String HONK_AND_OR_BLINK = "honkAndOrBlink";
public static final String REMOTE_HEATER = "remoteHeater";
public static final String PRECLIMATIZATION = "preclimatization";
public static final String LAST_TRIP_ID = "lastTripId";
// List of all adressable things in OH = SUPPORTED_DEVICE_THING_TYPES_UIDS + the virtual bridge
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(APIBRIDGE_THING_TYPE, VEHICLE_THING_TYPE).collect(Collectors.toSet());
// Default value for undefined integers
public static final int UNDEFINED = -1;
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/**
* Exception for errors when using the VolvoOnCall API
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallException extends Exception {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallException.class);
private static final long serialVersionUID = -6215621577081394328L;
public static enum ErrorType {
UNKNOWN,
SERVICE_UNAVAILABLE,
IOEXCEPTION,
JSON_SYNTAX;
}
private final ErrorType cause;
public VolvoOnCallException(String label, @Nullable String description) {
super(label);
if ("FoundationServicesUnavailable".equalsIgnoreCase(label)) {
cause = ErrorType.SERVICE_UNAVAILABLE;
} else {
cause = ErrorType.UNKNOWN;
logger.warn("Unhandled VoC error : {} : {}", label, description);
}
}
public VolvoOnCallException(Exception e) {
super(e);
if (e instanceof IOException) {
cause = ErrorType.IOEXCEPTION;
} else if (e instanceof JsonSyntaxException) {
cause = ErrorType.JSON_SYNTAX;
} else {
cause = ErrorType.UNKNOWN;
logger.warn("Unhandled VoC error : {}", e.getMessage());
}
}
public ErrorType getType() {
return cause;
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.discovery.VolvoOnCallDiscoveryService;
import org.openhab.binding.volvooncall.internal.handler.VehicleHandler;
import org.openhab.binding.volvooncall.internal.handler.VehicleStateDescriptionProvider;
import org.openhab.binding.volvooncall.internal.handler.VolvoOnCallBridgeHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VolvoOnCallHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.volvooncall", service = ThingHandlerFactory.class)
public class VolvoOnCallHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final VehicleStateDescriptionProvider stateDescriptionProvider;
@Activate
public VolvoOnCallHandlerFactory(@Reference VehicleStateDescriptionProvider provider) {
this.stateDescriptionProvider = provider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (APIBRIDGE_THING_TYPE.equals(thingTypeUID)) {
VolvoOnCallBridgeHandler bridgeHandler = new VolvoOnCallBridgeHandler((Bridge) thing);
registerDeviceDiscoveryService(bridgeHandler);
return bridgeHandler;
} else if (VEHICLE_THING_TYPE.equals(thingTypeUID)) {
return new VehicleHandler(thing, stateDescriptionProvider);
}
logger.warn("ThingHandler not found for {}", thing.getThingTypeUID());
return null;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof VolvoOnCallBridgeHandler) {
ThingUID thingUID = thingHandler.getThing().getUID();
unregisterDeviceDiscoveryService(thingUID);
}
super.removeHandler(thingHandler);
}
private void registerDeviceDiscoveryService(VolvoOnCallBridgeHandler bridgeHandler) {
VolvoOnCallDiscoveryService discoveryService = new VolvoOnCallDiscoveryService(bridgeHandler);
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
private void unregisterDeviceDiscoveryService(ThingUID thingUID) {
if (discoveryServiceRegs.containsKey(thingUID)) {
ServiceRegistration<?> serviceReg = discoveryServiceRegs.get(thingUID);
serviceReg.unregister();
discoveryServiceRegs.remove(thingUID);
}
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.action;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link IVolvoOnCallActions} defines the interface for all thing actions supported by the binding.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public interface IVolvoOnCallActions {
public void honkBlinkCommand(Boolean honk, Boolean blink);
public void preclimatizationStopCommand();
public void heaterStopCommand();
public void heaterStartCommand();
public void preclimatizationStartCommand();
public void engineStartCommand(@Nullable Integer runtime);
public void openCarCommand();
public void closeCarCommand();
}

View File

@@ -0,0 +1,206 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.action;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.handler.VehicleHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@VehicleAction } class is responsible to call corresponding
* action on Vehicle Handler
*
* @author Gaël L'hopital - Initial contribution
*/
@ThingActionsScope(name = "volvooncall")
@NonNullByDefault
public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallActions.class);
private @Nullable VehicleHandler handler;
public VolvoOnCallActions() {
logger.info("Volvo On Call actions service instanciated");
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof VehicleHandler) {
this.handler = (VehicleHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
@Override
@RuleAction(label = "Volvo On Call : Close", description = "Closes the car")
public void closeCarCommand() {
logger.debug("closeCarCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionClose();
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void closeCarCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).closeCarCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Open", description = "Opens the car")
public void openCarCommand() {
logger.debug("openCarCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionOpen();
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void openCarCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).openCarCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Start Engine", description = "Starts the engine")
public void engineStartCommand(@ActionInput(name = "runtime", label = "Runtime") @Nullable Integer runtime) {
logger.debug("engineStartCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionStart(runtime != null ? runtime : 5);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void engineStartCommand(@Nullable ThingActions actions, @Nullable Integer runtime) {
invokeMethodOf(actions).engineStartCommand(runtime);
}
@Override
@RuleAction(label = "Volvo On Call : Heater Start", description = "Starts car heater")
public void heaterStartCommand() {
logger.debug("heaterStartCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionHeater(true);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void heaterStartCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).heaterStartCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Preclimatization Start", description = "Starts car heater")
public void preclimatizationStartCommand() {
logger.debug("preclimatizationStartCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionPreclimatization(true);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void preclimatizationStartCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).preclimatizationStartCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Heater Stop", description = "Stops car heater")
public void heaterStopCommand() {
logger.debug("heaterStopCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionHeater(false);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void heaterStopCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).heaterStopCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Preclimatization Stop", description = "Stops car heater")
public void preclimatizationStopCommand() {
logger.debug("preclimatizationStopCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionPreclimatization(false);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void preclimatizationStopCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).preclimatizationStopCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Honk-blink", description = "Activates the horn and or lights of the car")
public void honkBlinkCommand(@ActionInput(name = "honk", label = "Honk") Boolean honk,
@ActionInput(name = "blink", label = "Blink") Boolean blink) {
logger.debug("honkBlinkCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionHonkBlink(honk, blink);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void honkBlinkCommand(@Nullable ThingActions actions, Boolean honk, Boolean blink) {
invokeMethodOf(actions).honkBlinkCommand(honk, blink);
}
private static IVolvoOnCallActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(VolvoOnCallActions.class.getName())) {
if (actions instanceof IVolvoOnCallActions) {
return (IVolvoOnCallActions) actions;
} else {
return (IVolvoOnCallActions) Proxy.newProxyInstance(IVolvoOnCallActions.class.getClassLoader(),
new Class[] { IVolvoOnCallActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of VolvoOnCallActions");
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link VehicleConfiguration} is the class used to match the
* Vehicle thing configuration.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VehicleConfiguration {
public String vin = "";
public Integer refresh = 5;
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.config;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link VolvoOnCallBridgeConfiguration} is responsible for holding
* configuration informations needed to access VOC API
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallBridgeConfiguration {
public String username = "";
public String password = "";
public String getAuthorization() {
byte[] authorization = Base64.getEncoder().encode((String.format("%s:%s", username, password)).getBytes());
return "Basic " + new String(authorization, StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.discovery;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.dto.AccountVehicleRelation;
import org.openhab.binding.volvooncall.internal.dto.Attributes;
import org.openhab.binding.volvooncall.internal.dto.Vehicles;
import org.openhab.binding.volvooncall.internal.handler.VolvoOnCallBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VolvoOnCallDiscoveryService} searches for available
* cars discoverable through VocAPI
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallDiscoveryService extends AbstractDiscoveryService {
private static final int SEARCH_TIME = 2;
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallDiscoveryService.class);
private final VolvoOnCallBridgeHandler bridgeHandler;
public VolvoOnCallDiscoveryService(VolvoOnCallBridgeHandler bridgeHandler) {
super(SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME);
this.bridgeHandler = bridgeHandler;
}
@Override
public void startScan() {
String[] relations = bridgeHandler.getVehiclesRelationsURL();
Arrays.stream(relations).forEach(relationURL -> {
try {
AccountVehicleRelation accountVehicle = bridgeHandler.getURL(relationURL, AccountVehicleRelation.class);
logger.debug("Found vehicle : {}", accountVehicle.vehicleId);
Vehicles vehicle = bridgeHandler.getURL(accountVehicle.vehicleURL, Vehicles.class);
Attributes attributes = bridgeHandler.getURL(Attributes.class, vehicle.vehicleId);
thingDiscovered(DiscoveryResultBuilder
.create(new ThingUID(VEHICLE_THING_TYPE, bridgeHandler.getThing().getUID(),
accountVehicle.vehicleId))
.withLabel(attributes.vehicleType + " " + attributes.registrationNumber)
.withBridge(bridgeHandler.getThing().getUID()).withProperty(VIN, attributes.vin)
.withRepresentationProperty(accountVehicle.vehicleId).build());
} catch (VolvoOnCallException e) {
logger.warn("Error while discovering vehicle: {}", e.getMessage());
}
});
stopScan();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* The {@link AccountVehicleRelation} is responsible for storing
* informations returned by vehicle position rest
* answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class AccountVehicleRelation extends VocAnswer {
@SerializedName("vehicle")
public @NonNullByDefault({}) String vehicleURL;
public @NonNullByDefault({}) String vehicleId;
/*
* Currently unused in the binding, maybe interesting in the future
* private String account;
* private String username;
* private String status;
* private Integer customerVehicleRelationId;
* private String accountId;
* private String accountVehicleRelation;
*/
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.util.List;
/**
* The {@link Attributes} is responsible for storing
* informations returned by vehicule attributes rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
public class Attributes extends VocAnswer {
public String vehicleType;
public String registrationNumber;
public Boolean carLocatorSupported;
public Boolean honkAndBlinkSupported;
public List<String> honkAndBlinkVersionsSupported;
public Boolean remoteHeaterSupported;
public Boolean unlockSupported;
public Boolean lockSupported;
public Boolean journalLogSupported;
public Integer unlockTimeFrame;
public Boolean journalLogEnabled;
public Boolean preclimatizationSupported;
public String vin;
public Boolean engineStartSupported;
/*
* Currently unused in the binding, maybe interesting in the future
* public class Country {
* public @NonNullByDefault({}) String iso2;
* }
* private String engineCode;
* private String exteriorCode;
* private String interiorCode;
* private String tyreDimensionCode;
* private Object tyreInflationPressureLightCode;
* private Object tyreInflationPressureHeavyCode;
* private String gearboxCode;
* private String fuelType;
* private Integer fuelTankVolume;
* private Integer grossWeight;
* private Integer modelYear;
* private String vehicleTypeCode;
* private Integer numberOfDoors;
* private Country country;
* private Integer carLocatorDistance;
* private Integer honkAndBlinkDistance;
* private String bCallAssistanceNumber;
* private Boolean assistanceCallSupported;
* private Integer verificationTimeFrame;
* private Integer timeFullyAccessible;
* private Integer timePartiallyAccessible;
* private String subscriptionType;
* private String subscriptionStartDate;
* private String subscriptionEndDate;
* private String serverVersion;
* private Boolean highVoltageBatterySupported;
* private Object maxActiveDelayChargingLocations;
* private Integer climatizationCalendarMaxTimers;
* private String vehiclePlatform;
* private Boolean statusParkedIndoorSupported;
* private Boolean overrideDelayChargingSupported;
* private @Nullable List<String> sendPOIToVehicleVersionsSupported = null;
* private @Nullable List<String> climatizationCalendarVersionsSupported = null;
*/
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link CustomerAccounts} is responsible for storing
* informations returned by customerAccount rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class CustomerAccounts extends VocAnswer {
@SerializedName("accountVehicleRelations")
public @NonNullByDefault({}) String[] accountVehicleRelationsURL;
public @Nullable String username;
/*
* Currently unused in the binding, maybe interesting in the future
* private String firstName;
* private String lastName;
* private String accountId;
* private String account;
*/
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OpenClosedType;
/**
* The {@link DoorsStatus} is responsible for storing
* informations returned by vehicle status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class DoorsStatus {
public @NonNullByDefault({}) OpenClosedType tailgateOpen;
public @NonNullByDefault({}) OpenClosedType rearRightDoorOpen;
public @NonNullByDefault({}) OpenClosedType rearLeftDoorOpen;
public @NonNullByDefault({}) OpenClosedType frontRightDoorOpen;
public @NonNullByDefault({}) OpenClosedType frontLeftDoorOpen;
public @NonNullByDefault({}) OpenClosedType hoodOpen;
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ERSStatus} is responsible for storing
* ERS Status informations returned by vehicule status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class ERSStatus {
public @NonNullByDefault({}) String status;
public @NonNullByDefault({}) ZonedDateTime timestamp;
public @NonNullByDefault({}) String engineStartWarning;
public @NonNullByDefault({}) ZonedDateTime engineStartWarningTimestamp;
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link Heater} is responsible for storing
* heater information returned by vehicle status rest answer
*
* @author Arie van der Lee - Initial contribution
*/
@NonNullByDefault
public class Heater {
private String status = "";
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
public State getStatus() {
if ("off".equalsIgnoreCase(status)) {
return OnOffType.OFF;
} else if ("on".equalsIgnoreCase(status) || "onOther".equalsIgnoreCase(status)) {
return OnOffType.ON;
}
return UnDefType.UNDEF;
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.UNDEFINED;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HvBattery} is responsible for storing
* PHEV Battery information returned by vehicle status rest answer
*
* @author Arie van der Lee - Initial contribution
*/
@NonNullByDefault
public class HvBattery {
public int hvBatteryLevel = UNDEFINED;
public int distanceToHVBatteryEmpty = UNDEFINED;
public @NonNullByDefault({}) String hvBatteryChargeStatusDerived;
public int timeToHVBatteryFullyCharged = UNDEFINED;
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Position} is responsible for storing
* informations returned by vehicle position rest
* answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Position extends VocAnswer {
public @NonNullByDefault({}) PositionData position;
public @NonNullByDefault({}) PositionData calculatedPosition;
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.time.ZonedDateTime;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link PositionData} is responsible for storing
* informations returned by vehicle position rest
* answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class PositionData {
public @Nullable Double longitude;
public @Nullable Double latitude;
private @Nullable ZonedDateTime timestamp;
public @Nullable String speed;
private @Nullable String heading;
public Boolean isHeading() {
return "true".equalsIgnoreCase(heading);
}
public Optional<ZonedDateTime> getTimestamp() {
ZonedDateTime timestamp = this.timestamp;
if (timestamp != null) {
return Optional.of(timestamp);
}
return Optional.empty();
}
/*
* Currently unused in the binding, maybe interesting in the future
* private String streetAddress;
* private String postalCode;
* private String city;
* private String iSO2CountryCode;
* private String region;
*/
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* The {@link PostResponse} is responsible for storing
* elements given back after a post to VOC api
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class PostResponse extends VocAnswer {
public static enum Status {
@SerializedName("Started")
STARTED,
@SerializedName("MessageDelivered")
DELIVERED,
@SerializedName("Failed")
FAILED,
@SerializedName("Successful")
SUCCESSFULL
}
public static enum ServiceType {
RHBLF, // Remote Honk and Blink Lights ?
RDU, // Remote door unlock
ERS, // Remote engine start
TN // Theft notification
}
public @NonNullByDefault({}) Status status;
public @NonNullByDefault({}) String vehicleId;
@SerializedName("service")
public @NonNullByDefault({}) String serviceURL;
public @NonNullByDefault({}) ServiceType serviceType;
/*
* Currently unused in the binding, maybe interesting in the future
*
* public static enum FailureReason {
*
* @SerializedName("TimeframePassed")
* TIME_FRAME_PASSED
* }
*
* private ZonedDateTime statusTimestamp;
* private ZonedDateTime startTime;
* private FailureReason failureReason;
*
* private Integer customerServiceId;
*/
}

View File

@@ -0,0 +1,140 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.UNDEFINED;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.OnOffType;
/**
* The {@link Status} is responsible for storing
* Door Status informations returned by vehicule status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Status extends VocAnswer {
public double averageFuelConsumption = UNDEFINED;
public int averageSpeed = UNDEFINED;
public int fuelAmount = UNDEFINED;
public int fuelAmountLevel = UNDEFINED;
public int distanceToEmpty = UNDEFINED;
public int odometer = UNDEFINED;
public int tripMeter1 = UNDEFINED;
public int tripMeter2 = UNDEFINED;
private @Nullable OnOffType carLocked;
private @Nullable OnOffType engineRunning;
public String brakeFluid = "";
public String washerFluidLevel = "";
private @Nullable WindowsStatus windows;
private @Nullable DoorsStatus doors;
private @Nullable TyrePressure tyrePressure;
private @Nullable HvBattery hvBattery;
private @Nullable Heater heater;
public String serviceWarningStatus = "";
private @NonNullByDefault({}) List<Object> bulbFailures;
public Optional<WindowsStatus> getWindows() {
WindowsStatus windows = this.windows;
if (windows != null) {
return Optional.of(windows);
}
return Optional.empty();
}
public Optional<DoorsStatus> getDoors() {
DoorsStatus doors = this.doors;
if (doors != null) {
return Optional.of(doors);
}
return Optional.empty();
}
public Optional<TyrePressure> getTyrePressure() {
TyrePressure tyrePressure = this.tyrePressure;
if (tyrePressure != null) {
return Optional.of(tyrePressure);
}
return Optional.empty();
}
public Optional<HvBattery> getHvBattery() {
HvBattery hvBattery = this.hvBattery;
if (hvBattery != null) {
return Optional.of(hvBattery);
}
return Optional.empty();
}
public Optional<Heater> getHeater() {
Heater heater = this.heater;
if (heater != null) {
return Optional.of(heater);
}
return Optional.empty();
}
public Optional<OnOffType> getCarLocked() {
OnOffType carLocked = this.carLocked;
if (carLocked != null) {
return Optional.of(carLocked);
}
return Optional.empty();
}
public Optional<OnOffType> getEngineRunning() {
OnOffType engineRunning = this.engineRunning;
if (engineRunning != null) {
return Optional.of(engineRunning);
}
return Optional.empty();
}
public boolean aFailedBulb() {
return bulbFailures.size() > 0;
}
/*
* Currently not used in the binding, maybe interesting for the future
*
* @SerializedName("ERS")
* private ERSStatus ers;
* private ZonedDateTime averageFuelConsumptionTimestamp;
* private ZonedDateTime averageSpeedTimestamp;
* private ZonedDateTime brakeFluidTimestamp;
* private ZonedDateTime bulbFailuresTimestamp;
* private ZonedDateTime carLockedTimestamp;
* private ZonedDateTime distanceToEmptyTimestamp;
* private ZonedDateTime engineRunningTimestamp;
* private ZonedDateTime fuelAmountLevelTimestamp;
* private ZonedDateTime fuelAmountTimestamp;
* private ZonedDateTime odometerTimestamp;
* private Boolean privacyPolicyEnabled;
* private ZonedDateTime privacyPolicyEnabledTimestamp;
* private String remoteClimatizationStatus;
* private ZonedDateTime remoteClimatizationStatusTimestamp;
* private ZonedDateTime serviceWarningStatusTimestamp;
* private Object theftAlarm;
* private String timeFullyAccessibleUntil;
* private String timePartiallyAccessibleUntil;
* private ZonedDateTime tripMeter1Timestamp;
* private ZonedDateTime tripMeter2Timestamp;
* private ZonedDateTime washerFluidLevelTimestamp;
*/
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Trip} is responsible for storing
* trip informations returned by trip rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Trip {
public int id;
public @NonNullByDefault({}) List<TripDetail> tripDetails;
@SerializedName("trip")
public @Nullable String tripURL;
/*
* Currently unused in the binding, maybe interesting in the future
* private String name;
* private String category;
* private String userNotes;
*/
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.UNDEFINED;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link TripDetail} is responsible for storing
* trip details returned by trip rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class TripDetail {
private @Nullable Integer fuelConsumption;
private @Nullable Integer electricalConsumption;
private @Nullable Integer electricalRegeneration;
public int distance = UNDEFINED;
public int startOdometer = UNDEFINED;
public int endOdometer = UNDEFINED;
private @Nullable ZonedDateTime endTime;
private @Nullable ZonedDateTime startTime;
private @NonNullByDefault({}) PositionData startPosition;
private @NonNullByDefault({}) PositionData endPosition;
private State ZonedDateTimeToState(@Nullable ZonedDateTime datetime) {
return datetime != null ? new DateTimeType(datetime.withZoneSameInstant(ZoneId.systemDefault()))
: UnDefType.NULL;
}
private State getPositionAsState(PositionData details) {
if (details.latitude != null && details.longitude != null) {
return new PointType(details.latitude + "," + details.longitude);
}
return UnDefType.NULL;
}
public State getStartTime() {
return ZonedDateTimeToState(startTime);
}
public State getStartPosition() {
return getPositionAsState(startPosition);
}
public State getEndTime() {
return ZonedDateTimeToState(endTime);
}
public State getEndPosition() {
return getPositionAsState(endPosition);
}
public long getDurationInMinutes() {
return Duration.between(startTime, endTime).toMinutes();
}
public Optional<Integer> getFuelConsumption() {
Integer fuelConsumption = this.fuelConsumption;
if (fuelConsumption != null) {
return Optional.of(fuelConsumption);
}
return Optional.empty();
}
public Optional<Integer> getElectricalConsumption() {
Integer electricalConsumption = this.electricalConsumption;
if (electricalConsumption != null) {
return Optional.of(electricalConsumption);
}
return Optional.empty();
}
public Optional<Integer> getElectricalRegeneration() {
Integer electricalRegeneration = this.electricalRegeneration;
if (electricalRegeneration != null) {
return Optional.of(electricalRegeneration);
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Trips} is responsible for storing
* trip informations returned by trip rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Trips extends VocAnswer {
public @Nullable List<Trip> trips;
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link TyrePressure} is responsible for storing
* Tyre Pressure informations returned by vehicule status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class TyrePressure {
public @NonNullByDefault({}) String frontLeftTyrePressure;
public @NonNullByDefault({}) String frontRightTyrePressure;
public @NonNullByDefault({}) String rearLeftTyrePressure;
public @NonNullByDefault({}) String rearRightTyrePressure;
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Vehicles} is responsible for storing
* informations returned by vehicule rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Vehicles extends VocAnswer {
public @NonNullByDefault({}) String vehicleId;
@SerializedName("attributes")
public @NonNullByDefault({}) String attributesURL;
@SerializedName("status")
public @NonNullByDefault({}) String statusURL;
/*
* Currently unused in the binding, maybe interesting in the future
*
*
* private String[] vehicleAccountRelations;
*/
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link VocAnswer} is the base class for all Voc API requests
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public abstract class VocAnswer {
private @Nullable String errorLabel;
private @Nullable String errorDescription;
public @Nullable String getErrorLabel() {
return errorLabel;
}
public @Nullable String getErrorDescription() {
return errorDescription;
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OpenClosedType;
/**
* The {@link WindowsStatus} is responsible for storing
* Windows Status informations returned by vehicule status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class WindowsStatus {
public @NonNullByDefault({}) OpenClosedType frontLeftWindowOpen;
public @NonNullByDefault({}) OpenClosedType frontRightWindowOpen;
public @NonNullByDefault({}) OpenClosedType rearLeftWindowOpen;
public @NonNullByDefault({}) OpenClosedType rearRightWindowOpen;
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
}

View File

@@ -0,0 +1,568 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.handler;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import static org.openhab.core.library.unit.MetricPrefix.KILO;
import static org.openhab.core.library.unit.SIUnits.*;
import static org.openhab.core.library.unit.SmartHomeUnits.*;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.action.VolvoOnCallActions;
import org.openhab.binding.volvooncall.internal.config.VehicleConfiguration;
import org.openhab.binding.volvooncall.internal.dto.Attributes;
import org.openhab.binding.volvooncall.internal.dto.DoorsStatus;
import org.openhab.binding.volvooncall.internal.dto.Heater;
import org.openhab.binding.volvooncall.internal.dto.HvBattery;
import org.openhab.binding.volvooncall.internal.dto.Position;
import org.openhab.binding.volvooncall.internal.dto.Status;
import org.openhab.binding.volvooncall.internal.dto.Trip;
import org.openhab.binding.volvooncall.internal.dto.TripDetail;
import org.openhab.binding.volvooncall.internal.dto.Trips;
import org.openhab.binding.volvooncall.internal.dto.TyrePressure;
import org.openhab.binding.volvooncall.internal.dto.Vehicles;
import org.openhab.binding.volvooncall.internal.dto.WindowsStatus;
import org.openhab.binding.volvooncall.internal.wrapper.VehiclePositionWrapper;
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.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/**
* The {@link VehicleHandler} is responsible for handling commands, which are sent
* to one of the channels.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VehicleHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
private final Map<String, String> activeOptions = new HashMap<>();
private @Nullable ScheduledFuture<?> refreshJob;
private Vehicles vehicle = new Vehicles();
private VehiclePositionWrapper vehiclePosition = new VehiclePositionWrapper(new Position());
private Status vehicleStatus = new Status();
private @NonNullByDefault({}) VehicleConfiguration configuration;
private Integer lastTripId = 0;
public VehicleHandler(Thing thing, VehicleStateDescriptionProvider stateDescriptionProvider) {
super(thing);
}
private Map<String, String> discoverAttributes(VolvoOnCallBridgeHandler bridgeHandler)
throws JsonSyntaxException, IOException, VolvoOnCallException {
Attributes attributes = bridgeHandler.getURL(vehicle.attributesURL, Attributes.class);
Map<String, String> properties = new HashMap<>();
properties.put(CAR_LOCATOR, attributes.carLocatorSupported.toString());
properties.put(HONK_AND_OR_BLINK, Boolean.toString(attributes.honkAndBlinkSupported
&& attributes.honkAndBlinkVersionsSupported.contains(HONK_AND_OR_BLINK)));
properties.put(HONK_BLINK, Boolean.toString(
attributes.honkAndBlinkSupported && attributes.honkAndBlinkVersionsSupported.contains(HONK_BLINK)));
properties.put(REMOTE_HEATER, attributes.remoteHeaterSupported.toString());
properties.put(UNLOCK, attributes.unlockSupported.toString());
properties.put(LOCK, attributes.lockSupported.toString());
properties.put(JOURNAL_LOG, Boolean.toString(attributes.journalLogSupported && attributes.journalLogEnabled));
properties.put(PRECLIMATIZATION, attributes.preclimatizationSupported.toString());
properties.put(ENGINE_START, attributes.engineStartSupported.toString());
properties.put(UNLOCK_TIME, attributes.unlockTimeFrame.toString());
return properties;
}
@Override
public void initialize() {
logger.trace("Initializing the Volvo On Call handler for {}", getThing().getUID());
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
configuration = getConfigAs(VehicleConfiguration.class);
try {
vehicle = bridgeHandler.getURL(SERVICE_URL + "vehicles/" + configuration.vin, Vehicles.class);
if (thing.getProperties().isEmpty()) {
Map<String, String> properties = discoverAttributes(bridgeHandler);
updateProperties(properties);
}
activeOptions.putAll(thing.getProperties().entrySet().stream().filter(p -> "true".equals(p.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
if (thing.getProperties().containsKey(LAST_TRIP_ID)) {
lastTripId = Integer.parseInt(thing.getProperties().get(LAST_TRIP_ID));
}
updateStatus(ThingStatus.ONLINE);
startAutomaticRefresh(configuration.refresh);
} catch (JsonSyntaxException | IOException | VolvoOnCallException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage());
}
}
}
/**
* Start the job refreshing the vehicle data
*
* @param refresh : refresh frequency in minutes
*/
private void startAutomaticRefresh(int refresh) {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob == null || refreshJob.isCancelled()) {
refreshJob = scheduler.scheduleWithFixedDelay(this::queryApiAndUpdateChannels, 10, refresh,
TimeUnit.MINUTES);
}
}
private void queryApiAndUpdateChannels() {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
try {
vehicleStatus = bridgeHandler.getURL(vehicle.statusURL, Status.class);
vehiclePosition = new VehiclePositionWrapper(bridgeHandler.getURL(Position.class, configuration.vin));
// Update all channels from the updated data
getThing().getChannels().stream().map(Channel::getUID)
.filter(channelUID -> isLinked(channelUID) && !LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
.forEach(channelUID -> {
State state = getValue(channelUID.getGroupId(), channelUID.getIdWithoutGroup(),
vehicleStatus, vehiclePosition);
updateState(channelUID, state);
});
updateTrips(bridgeHandler);
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
freeRefreshJob();
startAutomaticRefresh(configuration.refresh);
}
}
}
private void freeRefreshJob() {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob != null) {
refreshJob.cancel(true);
this.refreshJob = null;
}
}
@Override
public void dispose() {
freeRefreshJob();
super.dispose();
}
private void updateTrips(VolvoOnCallBridgeHandler bridgeHandler) throws VolvoOnCallException {
// This seems to rewind 100 days by default, did not find any way to filter it
Trips carTrips = bridgeHandler.getURL(Trips.class, configuration.vin);
List<Trip> tripList = carTrips.trips;
if (tripList != null) {
List<Trip> newTrips = tripList.stream().filter(trip -> trip.id >= lastTripId).collect(Collectors.toList());
Collections.reverse(newTrips);
logger.debug("Trips discovered : {}", newTrips.size());
if (!newTrips.isEmpty()) {
Integer newTripId = newTrips.get(newTrips.size() - 1).id;
if (newTripId > lastTripId) {
updateProperty(LAST_TRIP_ID, newTripId.toString());
lastTripId = newTripId;
}
newTrips.stream().map(t -> t.tripDetails.get(0)).forEach(catchUpTrip -> {
logger.debug("Trip found {}", catchUpTrip.getStartTime());
getThing().getChannels().stream().map(Channel::getUID).filter(
channelUID -> isLinked(channelUID) && LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
.forEach(channelUID -> {
State state = getTripValue(channelUID.getIdWithoutGroup(), catchUpTrip);
updateState(channelUID, state);
});
});
}
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channelID = channelUID.getIdWithoutGroup();
if (command instanceof RefreshType) {
queryApiAndUpdateChannels();
} else if (command instanceof OnOffType) {
OnOffType onOffCommand = (OnOffType) command;
if (ENGINE_START.equals(channelID) && onOffCommand == OnOffType.ON) {
actionStart(5);
} else if (REMOTE_HEATER.equals(channelID)) {
actionHeater(onOffCommand == OnOffType.ON);
} else if (PRECLIMATIZATION.equals(channelID)) {
actionPreclimatization(onOffCommand == OnOffType.ON);
} else if (CAR_LOCKED.equals(channelID)) {
if (onOffCommand == OnOffType.ON) {
actionClose();
} else {
actionOpen();
}
}
}
}
private State getTripValue(String channelId, TripDetail tripDetails) {
switch (channelId) {
case TRIP_CONSUMPTION:
return tripDetails.getFuelConsumption()
.map(value -> (State) new QuantityType<>(value.floatValue() / 100, LITRE))
.orElse(UnDefType.UNDEF);
case TRIP_DISTANCE:
return new QuantityType<>((double) tripDetails.distance / 1000, KILO(METRE));
case TRIP_START_TIME:
return tripDetails.getStartTime();
case TRIP_END_TIME:
return tripDetails.getEndTime();
case TRIP_DURATION:
return new QuantityType<>(tripDetails.getDurationInMinutes(), MINUTE);
case TRIP_START_ODOMETER:
return new QuantityType<>((double) tripDetails.startOdometer / 1000, KILO(METRE));
case TRIP_STOP_ODOMETER:
return new QuantityType<>((double) tripDetails.endOdometer / 1000, KILO(METRE));
case TRIP_START_POSITION:
return tripDetails.getStartPosition();
case TRIP_END_POSITION:
return tripDetails.getEndPosition();
}
return UnDefType.NULL;
}
private State getDoorsValue(String channelId, DoorsStatus doors) {
switch (channelId) {
case TAILGATE:
return doors.tailgateOpen;
case REAR_RIGHT:
return doors.rearRightDoorOpen;
case REAR_LEFT:
return doors.rearLeftDoorOpen;
case FRONT_RIGHT:
return doors.frontRightDoorOpen;
case FRONT_LEFT:
return doors.frontLeftDoorOpen;
case HOOD:
return doors.hoodOpen;
}
return UnDefType.NULL;
}
private State getWindowsValue(String channelId, WindowsStatus windows) {
switch (channelId) {
case REAR_RIGHT_WND:
return windows.rearRightWindowOpen;
case REAR_LEFT_WND:
return windows.rearLeftWindowOpen;
case FRONT_RIGHT_WND:
return windows.frontRightWindowOpen;
case FRONT_LEFT_WND:
return windows.frontLeftWindowOpen;
}
return UnDefType.NULL;
}
private State getTyresValue(String channelId, TyrePressure tyrePressure) {
switch (channelId) {
case REAR_RIGHT_TYRE:
return new StringType(tyrePressure.rearRightTyrePressure);
case REAR_LEFT_TYRE:
return new StringType(tyrePressure.rearLeftTyrePressure);
case FRONT_RIGHT_TYRE:
return new StringType(tyrePressure.frontRightTyrePressure);
case FRONT_LEFT_TYRE:
return new StringType(tyrePressure.frontLeftTyrePressure);
}
return UnDefType.NULL;
}
private State getHeaterValue(String channelId, Heater heater) {
switch (channelId) {
case REMOTE_HEATER:
case PRECLIMATIZATION:
return heater.getStatus();
}
return UnDefType.NULL;
}
private State getBatteryValue(String channelId, HvBattery hvBattery) {
switch (channelId) {
case BATTERY_LEVEL:
return hvBattery.hvBatteryLevel != UNDEFINED ? new QuantityType<>(hvBattery.hvBatteryLevel, PERCENT)
: UnDefType.UNDEF;
case BATTERY_DISTANCE_TO_EMPTY:
return hvBattery.distanceToHVBatteryEmpty != UNDEFINED
? new QuantityType<>(hvBattery.distanceToHVBatteryEmpty, KILO(METRE))
: UnDefType.UNDEF;
case CHARGE_STATUS:
return hvBattery.hvBatteryChargeStatusDerived != null
? new StringType(hvBattery.hvBatteryChargeStatusDerived)
: UnDefType.UNDEF;
case TIME_TO_BATTERY_FULLY_CHARGED:
return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED
? new QuantityType<>(hvBattery.timeToHVBatteryFullyCharged, MINUTE)
: UnDefType.UNDEF;
case CHARGING_END:
return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED && hvBattery.timeToHVBatteryFullyCharged > 0
? new DateTimeType(ZonedDateTime.now().plusMinutes(hvBattery.timeToHVBatteryFullyCharged))
: UnDefType.UNDEF;
}
return UnDefType.NULL;
}
private State getValue(@Nullable String groupId, String channelId, Status status, VehiclePositionWrapper position) {
switch (channelId) {
case ODOMETER:
return status.odometer != UNDEFINED ? new QuantityType<>((double) status.odometer / 1000, KILO(METRE))
: UnDefType.UNDEF;
case TRIPMETER1:
return status.tripMeter1 != UNDEFINED
? new QuantityType<>((double) status.tripMeter1 / 1000, KILO(METRE))
: UnDefType.UNDEF;
case TRIPMETER2:
return status.tripMeter2 != UNDEFINED
? new QuantityType<>((double) status.tripMeter2 / 1000, KILO(METRE))
: UnDefType.UNDEF;
case DISTANCE_TO_EMPTY:
return status.distanceToEmpty != UNDEFINED ? new QuantityType<>(status.distanceToEmpty, KILO(METRE))
: UnDefType.UNDEF;
case FUEL_AMOUNT:
return status.fuelAmount != UNDEFINED ? new QuantityType<>(status.fuelAmount, LITRE) : UnDefType.UNDEF;
case FUEL_LEVEL:
return status.fuelAmountLevel != UNDEFINED ? new QuantityType<>(status.fuelAmountLevel, PERCENT)
: UnDefType.UNDEF;
case FUEL_CONSUMPTION:
return status.averageFuelConsumption != UNDEFINED ? new DecimalType(status.averageFuelConsumption / 10)
: UnDefType.UNDEF;
case ACTUAL_LOCATION:
return position.getPosition();
case CALCULATED_LOCATION:
return position.isCalculated();
case HEADING:
return position.isHeading();
case LOCATION_TIMESTAMP:
return position.getTimestamp();
case CAR_LOCKED:
// Warning : carLocked is in the Doors group but is part of general status informations.
// Did not change it to avoid breaking change for users
return status.getCarLocked().map(State.class::cast).orElse(UnDefType.UNDEF);
case ENGINE_RUNNING:
return status.getEngineRunning().map(State.class::cast).orElse(UnDefType.UNDEF);
case BRAKE_FLUID_LEVEL:
return new StringType(status.brakeFluid);
case WASHER_FLUID_LEVEL:
return new StringType(status.washerFluidLevel);
case AVERAGE_SPEED:
return status.averageSpeed != UNDEFINED ? new QuantityType<>(status.averageSpeed, KILOMETRE_PER_HOUR)
: UnDefType.UNDEF;
case SERVICE_WARNING:
return new StringType(status.serviceWarningStatus);
case FUEL_ALERT:
return status.distanceToEmpty < 100 ? OnOffType.ON : OnOffType.OFF;
case BULB_FAILURE:
return status.aFailedBulb() ? OnOffType.ON : OnOffType.OFF;
case REMOTE_HEATER:
case PRECLIMATIZATION:
return status.getHeater().map(heater -> getHeaterValue(channelId, heater)).orElse(UnDefType.NULL);
}
if (groupId != null) {
switch (groupId) {
case GROUP_DOORS:
return status.getDoors().map(doors -> getDoorsValue(channelId, doors)).orElse(UnDefType.NULL);
case GROUP_WINDOWS:
return status.getWindows().map(windows -> getWindowsValue(channelId, windows))
.orElse(UnDefType.NULL);
case GROUP_TYRES:
return status.getTyrePressure().map(tyres -> getTyresValue(channelId, tyres))
.orElse(UnDefType.NULL);
case GROUP_BATTERY:
return status.getHvBattery().map(batteries -> getBatteryValue(channelId, batteries))
.orElse(UnDefType.NULL);
}
}
return UnDefType.NULL;
}
public void actionHonkBlink(Boolean honk, Boolean blink) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
StringBuilder url = new StringBuilder(SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/honk_blink/");
if (honk && blink && activeOptions.containsKey(HONK_BLINK)) {
url.append("both");
} else if (honk && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
url.append("horn");
} else if (blink && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
url.append("lights");
} else {
logger.warn("The vehicle is not capable of this action");
return;
}
try {
bridgeHandler.postURL(url.toString(), vehiclePosition.getPositionAsJSon());
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
}
private void actionOpenClose(String action, OnOffType controlState) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
if (activeOptions.containsKey(action)) {
if (!vehicleStatus.getCarLocked().isPresent() || vehicleStatus.getCarLocked().get() != controlState) {
try {
StringBuilder address = new StringBuilder(SERVICE_URL);
address.append("vehicles/");
address.append(configuration.vin);
address.append("/");
address.append(action);
bridgeHandler.postURL(address.toString(), "{}");
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
} else {
logger.info("The car {} is already {}ed", configuration.vin, action);
}
} else {
logger.warn("The car {} does not support remote {}ing", configuration.vin, action);
}
}
}
private void actionHeater(String action, Boolean start) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
if (activeOptions.containsKey(action)) {
try {
if (action.contains(REMOTE_HEATER)) {
String command = start ? "start" : "stop";
String address = SERVICE_URL + "vehicles/" + configuration.vin + "/heater/" + command;
bridgeHandler.postURL(address, start ? "{}" : null);
} else if (action.contains(PRECLIMATIZATION)) {
String command = start ? "start" : "stop";
String address = SERVICE_URL + "vehicles/" + configuration.vin + "/preclimatization/" + command;
bridgeHandler.postURL(address, start ? "{}" : null);
}
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
} else {
logger.warn("The car {} does not support {}", configuration.vin, action);
}
}
}
public void actionHeater(Boolean start) {
actionHeater(REMOTE_HEATER, start);
}
public void actionPreclimatization(Boolean start) {
actionHeater(PRECLIMATIZATION, start);
}
public void actionOpen() {
actionOpenClose(UNLOCK, OnOffType.OFF);
}
public void actionClose() {
actionOpenClose(LOCK, OnOffType.ON);
}
public void actionStart(Integer runtime) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
if (activeOptions.containsKey(ENGINE_START)) {
String url = SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/engine/start";
String json = "{\"runtime\":" + runtime.toString() + "}";
try {
bridgeHandler.postURL(url, json);
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
} else {
logger.warn("The car {} does not support remote engine starting", vehicle.vehicleId);
}
}
}
/*
* Called by Bridge when it has to notify this of a potential state
* update
*
*/
void updateIfMatches(String vin) {
if (vin.equalsIgnoreCase(configuration.vin)) {
queryApiAndUpdateChannels();
}
}
private @Nullable VolvoOnCallBridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
if (bridge != null) {
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
return (VolvoOnCallBridgeHandler) handler;
}
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
return null;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(VolvoOnCallActions.class);
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options for VehicleHandler.
*
* @author Gregory Moyer - Initial contribution
* @author Gaël L'hopital - Copied as-is in VolvoOnCall binding
*/
@Component(service = { DynamicStateDescriptionProvider.class, VehicleStateDescriptionProvider.class })
@NonNullByDefault
public class VehicleStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@@ -0,0 +1,201 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.handler;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Properties;
import java.util.Stack;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException.ErrorType;
import org.openhab.binding.volvooncall.internal.config.VolvoOnCallBridgeConfiguration;
import org.openhab.binding.volvooncall.internal.dto.CustomerAccounts;
import org.openhab.binding.volvooncall.internal.dto.PostResponse;
import org.openhab.binding.volvooncall.internal.dto.VocAnswer;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonSyntaxException;
/**
* The {@link VolvoOnCallBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallBridgeHandler extends BaseBridgeHandler {
private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(20);
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallBridgeHandler.class);
private final Properties httpHeader = new Properties();
private final List<ScheduledFuture<?>> pendingActions = new Stack<>();
private final Gson gson;
private @NonNullByDefault({}) CustomerAccounts customerAccount;
public VolvoOnCallBridgeHandler(Bridge bridge) {
super(bridge);
httpHeader.put("cache-control", "no-cache");
httpHeader.put("content-type", JSON_CONTENT_TYPE);
httpHeader.put("x-device-id", "Device");
httpHeader.put("x-originator-type", "App");
httpHeader.put("x-os-type", "Android");
httpHeader.put("x-os-version", "22");
httpHeader.put("Accept", "*/*");
gson = new GsonBuilder()
.registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
.parse(json.getAsJsonPrimitive().getAsString().replaceAll("\\+0000", "Z")))
.registerTypeAdapter(OpenClosedType.class,
(JsonDeserializer<OpenClosedType>) (json, type,
jsonDeserializationContext) -> json.getAsBoolean() ? OpenClosedType.OPEN
: OpenClosedType.CLOSED)
.registerTypeAdapter(OnOffType.class,
(JsonDeserializer<OnOffType>) (json, type,
jsonDeserializationContext) -> json.getAsBoolean() ? OnOffType.ON : OnOffType.OFF)
.create();
}
@Override
public void initialize() {
logger.debug("Initializing VolvoOnCall API bridge handler.");
VolvoOnCallBridgeConfiguration configuration = getConfigAs(VolvoOnCallBridgeConfiguration.class);
httpHeader.setProperty("Authorization", configuration.getAuthorization());
try {
customerAccount = getURL(SERVICE_URL + "customeraccounts/", CustomerAccounts.class);
if (customerAccount.username != null) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Incorrect username or password");
}
} catch (JsonSyntaxException | VolvoOnCallException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("VolvoOnCall Bridge is read-only and does not handle commands");
}
public String[] getVehiclesRelationsURL() {
if (customerAccount != null) {
return customerAccount.accountVehicleRelationsURL;
}
return new String[0];
}
public <T extends VocAnswer> T getURL(Class<T> objectClass, String vin) throws VolvoOnCallException {
String url = SERVICE_URL + "vehicles/" + vin + "/" + objectClass.getSimpleName().toLowerCase();
return getURL(url, objectClass);
}
public <T extends VocAnswer> T getURL(String url, Class<T> objectClass) throws VolvoOnCallException {
try {
String jsonResponse = HttpUtil.executeUrl("GET", url, httpHeader, null, JSON_CONTENT_TYPE, REQUEST_TIMEOUT);
logger.debug("Request for : {}", url);
logger.debug("Received : {}", jsonResponse);
T response = gson.fromJson(jsonResponse, objectClass);
String error = response.getErrorLabel();
if (error != null) {
throw new VolvoOnCallException(error, response.getErrorDescription());
}
return response;
} catch (JsonSyntaxException | IOException e) {
throw new VolvoOnCallException(e);
}
}
public class ActionResultControler implements Runnable {
PostResponse postResponse;
ActionResultControler(PostResponse postResponse) {
this.postResponse = postResponse;
}
@Override
public void run() {
switch (postResponse.status) {
case SUCCESSFULL:
case FAILED:
logger.info("Action status : {} for vehicle : {}.", postResponse.status.toString(),
postResponse.vehicleId);
getThing().getThings().stream().filter(VehicleHandler.class::isInstance)
.map(VehicleHandler.class::cast)
.forEach(handler -> handler.updateIfMatches(postResponse.vehicleId));
break;
default:
try {
postResponse = getURL(postResponse.serviceURL, PostResponse.class);
scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
} catch (VolvoOnCallException e) {
if (e.getType() == ErrorType.SERVICE_UNAVAILABLE) {
scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
}
}
}
}
}
void postURL(String URL, @Nullable String body) throws VolvoOnCallException {
InputStream inputStream = body != null ? new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)) : null;
try {
String jsonString = HttpUtil.executeUrl("POST", URL, httpHeader, inputStream, null, REQUEST_TIMEOUT);
logger.debug("Post URL: {} Attributes {}", URL, httpHeader);
PostResponse postResponse = gson.fromJson(jsonString, PostResponse.class);
String error = postResponse.getErrorLabel();
if (error == null) {
pendingActions
.add(scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS));
} else {
throw new VolvoOnCallException(error, postResponse.getErrorDescription());
}
pendingActions.removeIf(ScheduledFuture::isDone);
} catch (JsonSyntaxException | IOException e) {
throw new VolvoOnCallException(e);
}
}
@Override
public void dispose() {
super.dispose();
pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));
}
}

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.wrapper;
import java.time.ZoneId;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.dto.Position;
import org.openhab.binding.volvooncall.internal.dto.PositionData;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link VehiclePositionWrapper} stores provides utility functions
* over a {@link Position} provided by the rest API
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VehiclePositionWrapper {
private final Optional<PositionData> position;
private boolean isCalculated;
public VehiclePositionWrapper(Position vehicle) {
if (vehicle.calculatedPosition != null && vehicle.position.latitude != null) {
position = Optional.of(vehicle.position);
isCalculated = false;
} else if (vehicle.calculatedPosition != null && vehicle.calculatedPosition.latitude != null) {
position = Optional.of(vehicle.calculatedPosition);
isCalculated = true;
} else {
position = Optional.empty();
}
}
private State getPositionAsState(PositionData details) {
if (details.latitude != null && details.longitude != null) {
return new PointType(details.latitude + "," + details.longitude);
}
return UnDefType.NULL;
}
public State getPosition() {
return position.map(pos -> getPositionAsState(pos)).orElse(UnDefType.UNDEF);
}
public @Nullable String getPositionAsJSon() {
if (getPosition() != UnDefType.UNDEF) {
StringBuilder json = new StringBuilder("{\"clientLatitude\":");
json.append(position.get().latitude);
json.append(",\"clientLongitude\":");
json.append(position.get().longitude);
json.append(",\"clientAccuracy\":0}");
return json.toString();
}
return null;
}
public State isCalculated() {
return position.map(pos -> isCalculated ? (State) OnOffType.ON : OnOffType.OFF).orElse(UnDefType.UNDEF);
}
public State isHeading() {
return position.map(pos -> pos.isHeading() ? (State) OnOffType.ON : OnOffType.OFF).orElse(UnDefType.UNDEF);
}
public State getTimestamp() {
return position.flatMap(pos -> pos.getTimestamp())
.map(dt -> (State) new DateTimeType(dt.withZoneSameInstant(ZoneId.systemDefault())))
.orElse(UnDefType.NULL);
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="volvooncall" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>VolvoOnCall Binding</name>
<description>This binding enables the access to VolvoOnCall features.</description>
<author>Gaël L'hopital</author>
</binding:binding>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:volvooncall:vocapi">
<parameter name="username" type="text">
<label>Username</label>
<description>Your VOC username (email)</description>
<required>true</required>
</parameter>
<parameter name="password" type="text">
<label>Password</label>
<description>Your VOC password</description>
<required>true</required>
<context>password</context>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="volvooncall"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- VOC API Bridge -->
<bridge-type id="vocapi">
<label>VolvoOnCall API</label>
<description>This bridge represents the gateway to Volvo On Call API.</description>
<config-description-ref uri="thing-type:volvooncall:vocapi"/>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,391 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="volvooncall"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Volvo Vehicle -->
<thing-type id="vehicle">
<supported-bridge-type-refs>
<bridge-type-ref id="vocapi"/>
</supported-bridge-type-refs>
<label>Volvo Vehicle</label>
<channel-groups>
<channel-group id="doors" typeId="doors"/>
<channel-group id="windows" typeId="windows"/>
<channel-group id="odometer" typeId="odometer"/>
<channel-group id="tank" typeId="tank"/>
<channel-group id="position" typeId="position"/>
<channel-group id="tyrePressure" typeId="tyrePressure"/>
<channel-group id="battery" typeId="battery"/>
<channel-group id="other" typeId="other"/>
<channel-group id="lasttrip" typeId="lasttrip"/>
</channel-groups>
<representation-property>vin</representation-property>
<config-description>
<parameter name="vin" type="text" required="true">
<label>Vehicle Identification Number</label>
<description>VIN of the vehicle associated with this Thing</description>
</parameter>
<parameter name="refresh" type="integer" min="5" required="false">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in minutes.</description>
<default>5</default>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="other">
<label>Other</label>
<channels>
<channel id="averageSpeed" typeId="averageSpeed"/>
<channel id="engineRunning" typeId="engineRunning"/>
<channel id="remoteHeater" typeId="remoteHeater"/>
<channel id="preclimatization" typeId="preclimatization"/>
<channel id="brakeFluidLevel" typeId="brakeFluidLevel"/>
<channel id="washerFluidLevel" typeId="washerFluidLevel"/>
<channel id="serviceWarningStatus" typeId="serviceWarningStatus"/>
<channel id="bulbFailure" typeId="bulbFailure"/>
</channels>
</channel-group-type>
<channel-group-type id="lasttrip">
<label>Last Trip</label>
<channels>
<channel id="tripConsumption" typeId="fuelQuantity">
<label>Consumption</label>
<description>Indicates the quantity of fuel consumed by the trip</description>
</channel>
<channel id="tripDistance" typeId="odometer">
<label>Distance</label>
<description>Distance traveled</description>
</channel>
<channel id="tripStartTime" typeId="timestamp">
<label>Start Time</label>
<description>Trip start time</description>
</channel>
<channel id="tripEndTime" typeId="timestamp">
<label>End Time</label>
<description>Trip end time</description>
</channel>
<channel id="tripDuration" typeId="tripDuration"/>
<channel id="tripStartOdometer" typeId="odometer">
<label>Start Odometer</label>
</channel>
<channel id="tripStopOdometer" typeId="odometer">
<label>Stop Odometer</label>
</channel>
<channel id="startPosition" typeId="location">
<label>From</label>
<description>Starting location of the car</description>
</channel>
<channel id="endPosition" typeId="location">
<label>To</label>
<description>Stopping location of the car</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="doors">
<label>Doors Opening Status</label>
<channels>
<channel id="frontLeft" typeId="door">
<label>Front Left</label>
</channel>
<channel id="frontRight" typeId="door">
<label>Front Right</label>
</channel>
<channel id="rearLeft" typeId="door">
<label>Rear Left</label>
</channel>
<channel id="rearRight" typeId="door">
<label>Rear Right</label>
</channel>
<channel id="hood" typeId="door">
<label>Hood</label>
</channel>
<channel id="tailgate" typeId="door">
<label>Tailgate</label>
</channel>
<channel id="carLocked" typeId="carLocked"/>
</channels>
</channel-group-type>
<channel-group-type id="windows">
<label>Windows Opening Status</label>
<channels>
<channel id="frontLeftWnd" typeId="window">
<label>Front Left</label>
</channel>
<channel id="frontRightWnd" typeId="window">
<label>Front Right</label>
</channel>
<channel id="rearLeftWnd" typeId="window">
<label>Rear Left</label>
</channel>
<channel id="rearRightWnd" typeId="window">
<label>Rear Right</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="tyrePressure">
<label>Tyre pressure status</label>
<channels>
<channel id="frontLeftTyre" typeId="tyrePressure">
<label>Front Left</label>
</channel>
<channel id="frontRightTyre" typeId="tyrePressure">
<label>Front Right</label>
</channel>
<channel id="rearLeftTyre" typeId="tyrePressure">
<label>Rear Left</label>
</channel>
<channel id="rearRightTyre" typeId="tyrePressure">
<label>Rear Right</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="odometer">
<label>Trip Meters</label>
<channels>
<channel id="odometer" typeId="odometer"/>
<channel id="tripmeter1" typeId="odometer">
<label>Tripmeter 1</label>
</channel>
<channel id="tripmeter2" typeId="odometer">
<label>Tripmeter 2</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="tank">
<label>Tank Info</label>
<channels>
<channel id="fuelAmount" typeId="fuelQuantity">
<label>Fuel Amount</label>
<description>Indicates the quantity of fuel available in the tank</description>
</channel>
<channel id="fuelLevel" typeId="fuelLevel"/>
<channel id="fuelConsumption" typeId="fuelConsumption"/>
<channel id="fuelAlert" typeId="fuelAlert"/>
<channel id="distanceToEmpty" typeId="odometer">
<label>Distance Left</label>
<description>Distance left with given quantity of fuel</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="position">
<label>Location Info</label>
<channels>
<channel id="location" typeId="location">
<label>Location</label>
<description>The position of the vehicle</description>
</channel>
<channel id="calculatedLocation" typeId="calculatedLocation"/>
<channel id="heading" typeId="heading"/>
<channel id="locationTimestamp" typeId="timestamp">
<label>Location Timestamp</label>
<description>Timestamp of location value update</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="battery">
<label>Plugin Hybrid / Twin Engine info</label>
<channels>
<channel id="batteryLevel" typeId="batteryLevel"/>
<channel id="batteryDistanceToEmpty" typeId="odometer">
<label>Distance to battery empty</label>
</channel>
<channel id="chargeStatus" typeId="chargeStatus"/>
<channel id="timeToHVBatteryFullyCharged" typeId="timeToHVBatteryFullyCharged"/>
<channel id="chargingEnd" typeId="timestamp">
<label>Charging end</label>
</channel>
</channels>
</channel-group-type>
<channel-type id="door">
<item-type>Contact</item-type>
<label>Door</label>
<description>Indicates if the door is opened</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="tripDuration">
<item-type>Number:Time</item-type>
<label>Duration</label>
<description>Trip Duration</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="window">
<item-type>Contact</item-type>
<label>Window Status</label>
<description>Indicates if the window is opened</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="odometer">
<item-type>Number:Length</item-type>
<label>Odometer</label>
<description>Odometer of the vehicle</description>
<state pattern="%.2f %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="averageSpeed">
<item-type>Number:Speed</item-type>
<label>Average speed</label>
<description>Average speed of the vehicle</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="fuelQuantity">
<item-type>Number:Volume</item-type>
<label>Fuel Quantity</label>
<state pattern="%.2f %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="fuelLevel">
<item-type>Number:Dimensionless</item-type>
<label>Fuel Level</label>
<description>Indicates the level of fuel in the tank</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="fuelConsumption" advanced="true">
<item-type>Number</item-type>
<label>Average Consumption</label>
<description>Indicates the average fuel consumption in L/100km</description>
<state pattern="%.1f L/100km" readOnly="true"></state>
</channel-type>
<channel-type id="location">
<item-type>Location</item-type>
<label>Location</label>
<category>Location</category>
<state readOnly="true"></state>
</channel-type>
<channel-type id="calculatedLocation" advanced="true">
<item-type>Switch</item-type>
<label>Calculated Location</label>
<description>Indicates if the location is actual or calculated</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="heading" advanced="true">
<item-type>Switch</item-type>
<label>Heading</label>
<state readOnly="true"></state>
</channel-type>
<channel-type id="timestamp">
<item-type>DateTime</item-type>
<label>Timestamp</label>
<description>Data timestamp</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="engineStart">
<item-type>Switch</item-type>
<label>Start Engine</label>
<description>Starts the vehicle engine</description>
</channel-type>
<channel-type id="carLocked">
<item-type>Switch</item-type>
<label>Locked</label>
<description>Car locking status</description>
</channel-type>
<channel-type id="engineRunning">
<item-type>Switch</item-type>
<label>Engine Started</label>
<description>Engine status (running or not)</description>
</channel-type>
<channel-type id="preclimatization">
<item-type>Switch</item-type>
<label>Preclimatization</label>
<description>Starts pre-climatization</description>
</channel-type>
<channel-type id="remoteHeater">
<item-type>Switch</item-type>
<label>Remote Heater</label>
<description>(De)Activates remote heater</description>
</channel-type>
<channel-type id="bulbFailure">
<item-type>Switch</item-type>
<label>Bulb Failure</label>
<description>At least on bulb is reported as dead</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="brakeFluidLevel">
<item-type>String</item-type>
<label>Brake Fluid</label>
<description>“VeryLow”,"Low", "Normal"</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="washerFluidLevel">
<item-type>String</item-type>
<label>Washer Fluid</label>
<description>“VeryLow”,"Low", "Normal"</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="tyrePressure">
<item-type>String</item-type>
<label>Tyre pressure</label>
<description>“LowSoft”, "Normal"</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="serviceWarningStatus">
<item-type>String</item-type>
<label>Service Warning</label>
<description>Is car service needed ?</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="batteryLevel">
<item-type>Number:Dimensionless</item-type>
<label>Battery Level</label>
<description>Indicates the level of power in the battery (in case of PHEV / Twin Engine)</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="chargeStatus">
<item-type>String</item-type>
<label>Charging status</label>
<description>Status of charging (in case of PHEV / Twin Engine)</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="timeToHVBatteryFullyCharged">
<item-type>Number:Time</item-type>
<label>Time to battery fully charged</label>
<description>Time in seconds until the battery is fully charged (in case of PHEV / Twin Engine)</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="fuelAlert">
<item-type>Switch</item-type>
<label>Fuel Alarm</label>
<description>set to 'ON' when the tank level is low</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>