[groupepsa] Initial Contribution (#10332)

* Initial commit: template only

Signed-off-by: Arjan Mels <github@mels.email>

* Initial version

Signed-off-by: Arjan Mels <github@mels.email>

* Minor cleanup after rebase

Signed-off-by: Arjan Mels <github@mels.email>

* Corrected type "peugot" => "peugeot"

Signed-off-by: Arjan Mels <github@mels.email>

* Improved Exception messages
Corrected URL

Signed-off-by: Arjan Mels <github@mels.email>

* Switched from vin to id
Changed nextDelayedTime from ZonedDateTime to Duration
Added vehcile etails and additional info retrieval

Signed-off-by: Arjan Mels <github@mels.email>

* Before rebase

Signed-off-by: Arjan Mels <github@mels.email>

* Various fixes

Signed-off-by: Arjan Mels <github@mels.email>

* Updated tests

Signed-off-by: Arjan Mels <github@mels.email>

* Changed double to BigDecimal to keep accuracy
Added lastupdated channel
Small corrections

Signed-off-by: Arjan Mels <github@mels.email>

* Updated to 3.1.0-SNAPSHOT

Signed-off-by: Arjan Mels <github@mels.email>

* Added README.md

Signed-off-by: Arjan Mels <github@mels.email>

* Changed channels to lowerCamelCase

Signed-off-by: Arjan Mels <github@mels.email>

* Minor corrections

Signed-off-by: Arjan Mels <github@mels.email>

* Corrected lastUpdated in thing-types.xml

Signed-off-by: Arjan Mels <github@mels.email>

* Fixed ToStringbuilder

Signed-off-by: Arjan Mels <github@mels.email>

* corrected capitalization in thing-types.xml and Constants

Signed-off-by: Arjan Mels <github@mels.email>

* Corrected URL for Citroen

Signed-off-by: Arjan Mels <github@mels.email>

* Added groupepsa binding to bom pom.xml

Signed-off-by: Arjan Mels <github@mels.email>

* Updated copyrights

Signed-off-by: Arjan Mels <github@mels.email>

* Fixed CheckStyle warnings

Signed-off-by: Arjan Mels <github@mels.email>

* Fixed check style notifications

Signed-off-by: Arjan Mels <github@mels.email>

* Updated readme

Signed-off-by: Arjan Mels <github@mels.email>

* Corrected capitalization of thing-types.xml

Signed-off-by: Arjan Mels <github@mels.email>

* Updated pom. xml to 3.3.0-SNAPSHOT

Signed-off-by: Arjan Mels <github@mels.email>

* Updated copyright messages

Signed-off-by: Arjan Mels <github@mels.email>

* Various minor cleanups
Cleanup of the README.md
Removed unnecessary license header form the feature.xml file
Various null annotation corrections
Further cleanup of thing-types.xml
Cleanup of groupepsa.properties

Signed-off-by: Arjan Mels <github@mels.email>

* Logging updates

Signed-off-by: Arjan Mels <github@mels.email>

* Updated dependencies
Added dependencies to NOTICE

Signed-off-by: Arjan Mels <github@mels.email>

* Ignore lastposition for isConnected check

Signed-off-by: Arjan Mels <github@mels.email>

* Updated to use ThingHandlerService

Signed-off-by: Arjan Mels <github@mels.email>

* Fixed various review findings

Signed-off-by: Arjan Mels <github@mels.email>

Co-authored-by: Arjan Mels <github@mels.email>
This commit is contained in:
Arjan Mels
2022-04-30 19:44:53 +02:00
committed by GitHub
parent 7adfc482da
commit 8e7c78cef5
53 changed files with 4369 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.groupepsa-${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-groupepsa" description="Groupe PSA Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.groupepsa/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,109 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link GroupePSABindingConstants} class defines common constants, which
* are used across the whole binding.
*
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class GroupePSABindingConstants {
public static final String BINDING_ID = "groupepsa";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
public static final ThingTypeUID THING_TYPE_VEHICLE = new ThingTypeUID(BINDING_ID, "vehicle");
// Vehicle properties
public static final String VEHICLE_ID = "id";
public static final String VEHICLE_VIN = "vin";
public static final String VEHICLE_VENDOR = "vendor";
public static final String VEHICLE_MODEL = "model";
// List of all Channel ids
public static final String CHANNEL_BATTERY_CURRENT = "battery#current";
public static final String CHANNEL_BATTERY_VOLTAGE = "battery#voltage";
public static final String CHANNEL_TYPE_DOORLOCK = "doorLock";
public static final String CHANNEL_TYPE_DOOROPEN = "doorOpen";
public static final String CHANNEL_GROUP_DOORS = "doors";
public static final String CHANNEL_DOORS_LOCK = "doors#locked";
public static final String CHANNEL_ENVIRONMENT_TEMPERATURE = "environment#temperature";
public static final String CHANNEL_ENVIRONMENT_DAYTIME = "environment#daytime";
public static final String CHANNEL_MOTION_IGNITION = "motion#ignition";
public static final String CHANNEL_MOTION_ACCELERATION = "motion#acceleration";
public static final String CHANNEL_MOTION_MOVING = "motion#moving";
public static final String CHANNEL_MOTION_SPEED = "motion#speed";
public static final String CHANNEL_MOTION_MILEAGE = "motion#mileage";
public static final String CHANNEL_POSITION_POSITION = "position#position";
public static final String CHANNEL_POSITION_HEADING = "position#heading";
public static final String CHANNEL_POSITION_TYPE = "position#type";
public static final String CHANNEL_POSITION_SIGNALSTRENGTH = "position#signal";
public static final String CHANNEL_VARIOUS_LAST_UPDATED = "various#lastUpdated";
public static final String CHANNEL_VARIOUS_PRIVACY = "various#privacy";
public static final String CHANNEL_VARIOUS_BELT = "various#belt";
public static final String CHANNEL_VARIOUS_EMERGENCY = "various#emergency";
public static final String CHANNEL_VARIOUS_SERVICE = "various#service";
public static final String CHANNEL_VARIOUS_PRECONDITINING = "various#preconditioning";
public static final String CHANNEL_VARIOUS_PRECONDITINING_FAILURE = "various#preconditioningFailure";
public static final String CHANNEL_FUEL_AUTONOMY = "fuel#autonomy";
public static final String CHANNEL_FUEL_CONSUMPTION = "fuel#consumption";
public static final String CHANNEL_FUEL_LEVEL = "fuel#level";
public static final String CHANNEL_ELECTRIC_AUTONOMY = "electric#autonomy";
public static final String CHANNEL_ELECTRIC_LEVEL = "electric#level";
public static final String CHANNEL_ELECTRIC_RESIDUAL = "electric#residual";
public static final String CHANNEL_ELECTRIC_BATTERY_CAPACITY = "electric#batteryCapacity";
public static final String CHANNEL_ELECTRIC_BATTERY_HEALTH_CAPACITY = "electric#batteryHealthCapacity";
public static final String CHANNEL_ELECTRIC_BATTERY_HEALTH_RESISTANCE = "electric#batteryHealthResistance";
public static final String CHANNEL_ELECTRIC_CHARGING_STATUS = "electric#chargingStatus";
public static final String CHANNEL_ELECTRIC_CHARGING_MODE = "electric#chargingMode";
public static final String CHANNEL_ELECTRIC_CHARGING_PLUGGED = "electric#chargingPlugged";
public static final String CHANNEL_ELECTRIC_CHARGING_RATE = "electric#chargingRate";
public static final String CHANNEL_ELECTRIC_CHARGING_REMAININGTIME = "electric#chargingRemainingTime";
public static final String CHANNEL_ELECTRIC_CHARGING_NEXTDELAYEDTIME = "electric#chargingNextDelayedTime";
public enum VendorConstants {
PEUGEOT("https://idpcvs.peugeot.com/am/oauth2/access_token", "clientsB2CPeugeot"),
CITROEN("https://idpcvs.citroen.com/am/oauth2/access_token", "clientsB2CCitroen"),
DS("https://idpcvs.driveds.com/am/oauth2/access_token", "clientsB2CDS"),
OPEL("https://idpcvs.opel.com/am/oauth2/access_token", "clientsB2COpel"),
VAUXHALL("https://idpcvs.vauxhall.co.uk/am/oauth2/access_token", "clientsB2CVauxhall");
public final String url;
public final String realm;
public final String scope;
VendorConstants(String url, String realm) {
this.url = url;
this.realm = realm;
this.scope = "profile openid";
}
}
public static final String API_URL = "https://api.groupe-psa.com/connectedcar/v4";
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal;
import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.groupepsa.internal.bridge.GroupePSABridgeHandler;
import org.openhab.binding.groupepsa.internal.things.GroupePSAHandler;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link GroupePSAHandlerFactory} is responsible for creating things and
* thing handlers.
*
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.groupepsa", service = ThingHandlerFactory.class)
public class GroupePSAHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_VEHICLE);
private final OAuthFactory oAuthFactory;
protected final HttpClient httpClient;
@Activate
public GroupePSAHandlerFactory(@Reference OAuthFactory oAuthFactory,
@Reference HttpClientFactory httpClientFactory) {
this.oAuthFactory = oAuthFactory;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@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 (THING_TYPE_VEHICLE.equals(thingTypeUID)) {
return new GroupePSAHandler(thing);
} else if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
return new GroupePSABridgeHandler((Bridge) thing, oAuthFactory, httpClient);
}
return null;
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.bridge;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GroupePSABridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public final class GroupePSABridgeConfiguration {
private String vendor = "";
private String userName = "";
private String password = "";
private String clientId = "";
private String clientSecret = "";
private Integer pollingInterval = 5;
/**
* @return The polling interval for the groupepsa state in s
*/
public Integer getPollingInterval() {
return pollingInterval;
}
public void setPollingInterval(Integer pollingInterval) {
this.pollingInterval = pollingInterval;
}
public String getVendor() {
return vendor;
}
public void setVendor(String vendor) {
this.vendor = vendor;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
}

View File

@@ -0,0 +1,225 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.bridge;
import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.THING_TYPE_BRIDGE;
import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.VendorConstants;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.groupepsa.internal.discovery.GroupePSADiscoveryService;
import org.openhab.binding.groupepsa.internal.rest.api.GroupePSAConnectApi;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Vehicle;
import org.openhab.binding.groupepsa.internal.rest.api.dto.VehicleStatus;
import org.openhab.binding.groupepsa.internal.rest.exceptions.GroupePSACommunicationException;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
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.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
/**
* The {@link GroupePSABridgeHandler} is responsible for handling commands,
* which are sent to one of the channels.
*
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class GroupePSABridgeHandler extends BaseBridgeHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
private static final long DEFAULT_POLLING_INTERVAL_M = TimeUnit.HOURS.toMinutes(1);
private final OAuthFactory oAuthFactory;
private @Nullable OAuthClientService oAuthService;
private @Nullable ScheduledFuture<?> groupepsaBridgePollingJob;
private final HttpClient httpClient;
private String vendor = "";
private @Nullable VendorConstants vendorConstants;
private String userName = "";
private String password = "";
private String clientId = "";
private String clientSecret = "";
private @Nullable GroupePSAConnectApi groupePSAApi;
public GroupePSABridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
super(bridge);
this.oAuthFactory = oAuthFactory;
this.httpClient = httpClient;
}
private void pollGroupePSAs() {
try {
List<Vehicle> vehicles = getVehicles();
if (vehicles != null) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/comm-error-query-vehicles-failed");
}
} catch (GroupePSACommunicationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
@Override
public void dispose() {
stopGroupePSABridgePolling();
oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
}
@Override
public void initialize() {
GroupePSABridgeConfiguration bridgeConfiguration = getConfigAs(GroupePSABridgeConfiguration.class);
vendor = bridgeConfiguration.getVendor();
userName = bridgeConfiguration.getUserName();
password = bridgeConfiguration.getPassword();
clientId = bridgeConfiguration.getClientId();
clientSecret = bridgeConfiguration.getClientSecret();
final Integer pollingIntervalM = bridgeConfiguration.getPollingInterval();
if (vendor.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-vendor");
} else if (userName.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-username");
} else if (password.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-password");
} else if (clientId.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-clientid");
} else if (clientSecret.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/conf-error-no-clientsecret");
} else if (pollingIntervalM < 1) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/conf-error-invalid-polling-interval");
} else {
VendorConstants localVendorConstants = VendorConstants.valueOf(vendor);
vendorConstants = localVendorConstants;
oAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(), localVendorConstants.url,
null, clientId, clientSecret, localVendorConstants.scope, true);
groupePSAApi = new GroupePSAConnectApi(httpClient, this, clientId, localVendorConstants.realm);
startGroupePSABridgePolling(pollingIntervalM);
updateStatus(ThingStatus.UNKNOWN);
}
}
private void startGroupePSABridgePolling(@Nullable Integer pollingIntervalM) {
if (groupepsaBridgePollingJob == null) {
final long pollingIntervalToUse = pollingIntervalM == null ? DEFAULT_POLLING_INTERVAL_M : pollingIntervalM;
groupepsaBridgePollingJob = scheduler.scheduleWithFixedDelay(() -> pollGroupePSAs(), 1,
TimeUnit.MINUTES.toSeconds(pollingIntervalToUse), TimeUnit.SECONDS);
}
}
private void stopGroupePSABridgePolling() {
final ScheduledFuture<?> job = groupepsaBridgePollingJob;
if (job != null) {
job.cancel(true);
groupepsaBridgePollingJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
static Throwable getRootCause(Throwable e) {
Throwable nextE;
do {
nextE = e.getCause();
if (nextE != null) {
e = nextE;
}
} while (nextE != null);
return e;
}
public String authenticate() throws GroupePSACommunicationException {
OAuthClientService localOAuthService = oAuthService;
VendorConstants localVendorConstants = vendorConstants;
if (localOAuthService == null) {
throw new GroupePSACommunicationException("OAuth service is unexpectedly null");
}
if (localVendorConstants == null) {
throw new GroupePSACommunicationException("Vendor constants are unexpectedly null");
}
try {
AccessTokenResponse result = localOAuthService.getAccessTokenResponse();
if (result == null) {
result = localOAuthService.getAccessTokenByResourceOwnerPasswordCredentials(userName, password,
localVendorConstants.scope);
}
return result.getAccessToken();
} catch (OAuthException | IOException | OAuthResponseException e) {
throw new GroupePSACommunicationException("Unable to authenticate: " + getRootCause(e).getMessage(), e);
}
}
public GroupePSAConnectApi getAPI() throws GroupePSACommunicationException {
GroupePSAConnectApi localGroupePSAApi = groupePSAApi;
if (localGroupePSAApi == null) {
throw new GroupePSACommunicationException("groupePSAApi is unexpectedly null");
} else {
return localGroupePSAApi;
}
}
/**
* @return A list of vehicles
* @throws GroupePSACommunicationException In case the query cannot be executed
* successfully
*/
public @Nullable List<Vehicle> getVehicles() throws GroupePSACommunicationException {
return getAPI().getVehicles();
}
/**
* @param id The id of the mower to query
* @return A detailed status of the mower with the specified id
* @throws GroupePSACommunicationException In case the query cannot be executed
* successfully
*/
public @Nullable VehicleStatus getVehicleStatus(String vin) throws GroupePSACommunicationException {
return getAPI().getVehicleStatus(vin);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(GroupePSADiscoveryService.class);
}
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.discovery;
import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.THING_TYPE_VEHICLE;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.groupepsa.internal.GroupePSABindingConstants;
import org.openhab.binding.groupepsa.internal.bridge.GroupePSABridgeHandler;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Vehicle;
import org.openhab.binding.groupepsa.internal.rest.exceptions.GroupePSACommunicationException;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link GroupePSADiscoveryService} is responsible for discovering new
* vehicles available for the configured app key.
*
* @author Arjan Mels - Initial contribution
*/
@Component(service = ThingHandlerService.class)
@NonNullByDefault
public class GroupePSADiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(GroupePSADiscoveryService.class);
private @Nullable GroupePSABridgeHandler bridgeHandler;
public GroupePSADiscoveryService() {
super(Collections.singleton(THING_TYPE_VEHICLE), 10, false);
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof GroupePSABridgeHandler) {
bridgeHandler = (GroupePSABridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
protected void startScan() {
try {
GroupePSABridgeHandler localBridgeHandler = bridgeHandler;
if (localBridgeHandler == null) {
return;
}
List<Vehicle> vehicles = localBridgeHandler.getVehicles();
if (vehicles == null || vehicles.isEmpty()) {
logger.warn("No vehicles found");
return;
}
for (Vehicle vehicle : vehicles) {
ThingUID bridgeUID = localBridgeHandler.getThing().getUID();
ThingTypeUID thingTypeUID = THING_TYPE_VEHICLE;
String id = vehicle.getId();
if (id != null) {
ThingUID vehicleThingUid = new ThingUID(THING_TYPE_VEHICLE, bridgeUID, id);
Map<String, Object> properties = new HashMap<>();
putProperty(properties, GroupePSABindingConstants.VEHICLE_ID, id);
putProperty(properties, GroupePSABindingConstants.VEHICLE_VIN, vehicle.getVin());
putProperty(properties, GroupePSABindingConstants.VEHICLE_VENDOR, vehicle.getBrand());
putProperty(properties, GroupePSABindingConstants.VEHICLE_MODEL, vehicle.getLabel());
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(vehicleThingUid)
.withThingType(thingTypeUID).withProperties(properties).withBridge(bridgeUID)
.withRepresentationProperty(GroupePSABindingConstants.VEHICLE_VIN)
.withLabel(vehicle.getBrand() + " (" + vehicle.getVin() + ")").build();
thingDiscovered(discoveryResult);
}
}
} catch (GroupePSACommunicationException e) {
logger.warn("No vehicles found", e);
return;
}
}
private void putProperty(Map<String, Object> properties, String key, @Nullable String value) {
if (value == null) {
value = "Unknown";
}
properties.put(key, value);
}
}

View File

@@ -0,0 +1,200 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api;
import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.API_URL;
import java.lang.reflect.Type;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.groupepsa.internal.bridge.GroupePSABridgeHandler;
import org.openhab.binding.groupepsa.internal.rest.api.dto.ErrorObject;
import org.openhab.binding.groupepsa.internal.rest.api.dto.User;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Vehicle;
import org.openhab.binding.groupepsa.internal.rest.api.dto.VehicleStatus;
import org.openhab.binding.groupepsa.internal.rest.exceptions.GroupePSACommunicationException;
import org.openhab.binding.groupepsa.internal.rest.exceptions.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.filosganga.geogson.gson.GeometryAdapterFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
/**
* Allows access to the GroupePSAConnectApi
*
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class GroupePSAConnectApi {
private final Logger logger = LoggerFactory.getLogger(GroupePSAConnectApi.class);
private final HttpClient httpClient;
private final GroupePSABridgeHandler bridge;
private final String clientId;
private final String realm;
protected final Gson gson;
public GroupePSAConnectApi(HttpClient httpClient, GroupePSABridgeHandler bridge, String clientId, String realm) {
this.httpClient = httpClient;
this.bridge = bridge;
this.clientId = clientId;
this.realm = realm;
gson = new GsonBuilder().registerTypeAdapterFactory(new GeometryAdapterFactory())
.registerTypeAdapter(ZonedDateTime.class, new JsonDeserializer<ZonedDateTime>() {
@Override
public @Nullable ZonedDateTime deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
return ZonedDateTime.parse(json.getAsJsonPrimitive().getAsString());
}
}).registerTypeAdapter(Duration.class, new JsonDeserializer<Duration>() {
@Override
public @Nullable Duration deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
return Duration.parse(json.getAsJsonPrimitive().getAsString());
}
}).create();
}
protected HttpClient getHttpClient() {
return httpClient;
}
protected GroupePSABridgeHandler getBridge() {
return bridge;
}
public String getBaseUrl() {
return API_URL;
}
private ContentResponse executeRequest(final String uri) throws GroupePSACommunicationException {
return executeRequest(uri, "application/hal+json");
}
static Throwable getRootCause(Throwable e) {
Throwable nextE;
do {
nextE = e.getCause();
if (nextE != null) {
e = nextE;
}
} while (nextE != null);
return e;
}
public ContentResponse executeRequest(final String uri, final String accept)
throws GroupePSACommunicationException {
Request request = getHttpClient().newRequest(uri);
String token = getBridge().authenticate();
request.timeout(10, TimeUnit.SECONDS);
request.param("client_id", this.clientId);
request.header("Authorization", "Bearer " + token);
request.header("Accept", accept);
request.header("x-introspect-realm", this.realm);
request.method(HttpMethod.GET);
logger.trace("HttpRequest {}", request.getURI());
logger.trace("HttpRequest Headers:\n{}", request.getHeaders());
try {
ContentResponse response = request.send();
logger.trace("HttpResponse {}", response);
logger.trace("HttpResponse Headers:\n{}", response.getHeaders());
logger.trace("HttpResponse Content: {}", response.getContentAsString());
return response;
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new GroupePSACommunicationException("Unable to perform Http Request: " + getRootCause(e).getMessage(),
e);
}
}
private void checkForError(ContentResponse response, int statusCode) throws GroupePSACommunicationException {
if (statusCode >= 200 && statusCode < 300) {
return;
}
switch (statusCode) {
case HttpStatus.NOT_FOUND_404:
ErrorObject error = gson.fromJson(response.getContentAsString(), ErrorObject.class);
String message = (error != null) ? error.getMessage() : null;
if (message == null) {
message = "Unknown";
}
throw new GroupePSACommunicationException(statusCode, message);
case HttpStatus.FORBIDDEN_403:
case HttpStatus.UNAUTHORIZED_401:
throw new UnauthorizedException(statusCode, response.getContentAsString());
default:
throw new GroupePSACommunicationException(statusCode, response.getContentAsString());
}
}
private <T> @Nullable T parseResponse(ContentResponse response, Class<T> type)
throws GroupePSACommunicationException {
int statusCode = response.getStatus();
checkForError(response, statusCode);
try {
return gson.fromJson(response.getContentAsString(), type);
} catch (JsonSyntaxException e) {
throw new GroupePSACommunicationException("Error in received JSON: " + getRootCause(e).getMessage(), e);
}
}
public @Nullable List<Vehicle> getVehicles() throws GroupePSACommunicationException {
ContentResponse response = executeRequest(getBaseUrl() + "/user");
User user = parseResponse(response, User.class);
if (user != null) {
return user.getVehicles();
} else {
return null;
}
}
public @Nullable VehicleStatus getVehicleStatus(String vin) throws GroupePSACommunicationException {
ContentResponse responseOdometer = executeRequest(getBaseUrl() + "/user/vehicles/" + vin + "/status");
VehicleStatus status = parseResponse(responseOdometer, VehicleStatus.class);
return status;
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Air {
private @Nullable BigDecimal temp;
public @Nullable BigDecimal getTemp() {
return temp;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("temp", temp).toString();
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class AirConditioning {
private @Nullable String failureCause;
private @Nullable List<Program> programs = null;
private @Nullable String status;
private @Nullable ZonedDateTime updatedAt;
public @Nullable String getFailureCause() {
return failureCause;
}
public @Nullable List<Program> getPrograms() {
return programs;
}
public @Nullable String getStatus() {
return status;
}
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("failureCause", failureCause).append("programs", programs)
.append("status", status).append("updatedAt", updatedAt).toString();
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Battery {
private @Nullable ZonedDateTime createdAt;
private @Nullable BigDecimal current;
private @Nullable BigDecimal voltage;
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable BigDecimal getCurrent() {
return current;
}
public @Nullable BigDecimal getVoltage() {
return voltage;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("createdAt", createdAt).append("current", current)
.append("voltage", voltage).toString();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class BatteryStatus {
private @Nullable BigDecimal capacity;
private @Nullable Health health;
public @Nullable BigDecimal getCapacity() {
return capacity;
}
public @Nullable Health getHealth() {
return health;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("capacity", capacity).append("health", health).toString();
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Charging {
private @Nullable String chargingMode;
private @Nullable BigDecimal chargingRate;
private @Nullable Duration nextDelayedTime;
private @Nullable Boolean plugged;
private @Nullable Duration remainingTime;
private @Nullable String status;
public @Nullable String getChargingMode() {
return chargingMode;
}
public @Nullable BigDecimal getChargingRate() {
return chargingRate;
}
public void setChargingRate(BigDecimal chargingRate) {
this.chargingRate = chargingRate;
}
public @Nullable Duration getNextDelayedTime() {
return nextDelayedTime;
}
public @Nullable Boolean isPlugged() {
return plugged;
}
public @Nullable Duration getRemainingTime() {
return remainingTime;
}
public @Nullable String getStatus() {
return status;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("chargingMode", chargingMode).append("chargingRate", chargingRate)
.append("nextDelayedTime", nextDelayedTime).append("plugged", plugged)
.append("remainingTime", remainingTime).append("status", status).toString();
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class DoorsState {
private @Nullable List<String> lockedState = null;
private @Nullable List<Opening> opening = null;
private @Nullable ZonedDateTime updatedAt;
public @Nullable List<String> getLockedState() {
return lockedState;
}
public @Nullable List<Opening> getOpening() {
return opening;
}
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("lockedState", lockedState).append("opening", opening)
.append("updatedAt", updatedAt).toString();
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Energy {
private @Nullable ZonedDateTime updatedAt;
private @Nullable ZonedDateTime createdAt;
private @Nullable BigDecimal autonomy;
private @Nullable BatteryStatus battery;
private @Nullable Charging charging;
private @Nullable BigDecimal consumption;
private @Nullable BigDecimal level;
private @Nullable BigDecimal residual;
private @Nullable String type;
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable BigDecimal getAutonomy() {
return autonomy;
}
public @Nullable BatteryStatus getBattery() {
return battery;
}
public @Nullable Charging getCharging() {
return charging;
}
public @Nullable BigDecimal getConsumption() {
return consumption;
}
public @Nullable BigDecimal getLevel() {
return level;
}
public @Nullable BigDecimal getResidual() {
return residual;
}
public @Nullable String getType() {
return type;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("updatedAt", updatedAt).append("createdAt", createdAt)
.append("autonomy", autonomy).append("battery", battery).append("charging", charging)
.append("consumption", consumption).append("level", level).append("residual", residual)
.append("type", type).toString();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Engine {
@SerializedName("class")
private @Nullable String type;
private @Nullable String energy;
public @Nullable String getType() {
return type;
}
public @Nullable String getEnergy() {
return energy;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("type", type).append("energy", energy).toString();
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Environment {
private @Nullable ZonedDateTime updatedAt;
private @Nullable ZonedDateTime createdAt;
private @Nullable Air air;
private @Nullable Luminosity luminosity;
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable Air getAir() {
return air;
}
public @Nullable Luminosity getLuminosity() {
return luminosity;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("updatedAt", updatedAt).append("createdAt", createdAt)
.append("air", air).append("luminosity", luminosity).toString();
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class ErrorObject {
private @Nullable String uuid;
private @Nullable String code;
private @Nullable String message;
public @Nullable String getUuid() {
return uuid;
}
public @Nullable String getMessage() {
return message;
}
public @Nullable String getCode() {
return code;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("uuid", uuid).append("code", code).append("message", message)
.toString();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Health {
private @Nullable BigDecimal capacity;
private @Nullable BigDecimal resistance;
public @Nullable BigDecimal getCapacity() {
return capacity;
}
public @Nullable BigDecimal getResistance() {
return resistance;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("capacity", capacity).append("resistance", resistance).toString();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Ignition {
private @Nullable ZonedDateTime updatedAt;
private @Nullable String type;
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
public @Nullable String getType() {
return type;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("updatedAt", updatedAt).append("type", type).toString();
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Kinetic {
private @Nullable ZonedDateTime createdAt;
private @Nullable BigDecimal acceleration;
private @Nullable Boolean moving;
private @Nullable BigDecimal pace;
private @Nullable BigDecimal speed;
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable BigDecimal getAcceleration() {
return acceleration;
}
public @Nullable Boolean isMoving() {
return moving;
}
public @Nullable BigDecimal getPace() {
return pace;
}
public @Nullable BigDecimal getSpeed() {
return speed;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("createdAt", createdAt).append("acceleration", acceleration)
.append("moving", moving).append("pace", pace).append("speed", speed).toString();
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Luminosity {
private @Nullable Boolean day;
public @Nullable Boolean isDay() {
return day;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("day", day).toString();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Occurence {
private @Nullable List<String> day = null;
private @Nullable List<String> week = null;
public @Nullable List<String> getDay() {
return day;
}
public @Nullable List<String> getWeek() {
return week;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("day", day).append("week", week).toString();
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Odometer {
private @Nullable ZonedDateTime createdAt;
private @Nullable BigDecimal mileage;
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable BigDecimal getMileage() {
return mileage;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("createdAt", createdAt).append("mileage", mileage).toString();
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Opening {
private @Nullable String identifier;
private @Nullable String state;
public @Nullable String getIdentifier() {
return identifier;
}
public @Nullable String getState() {
return state;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("identifier", identifier).append("state", state).toString();
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.github.filosganga.geogson.model.Geometry;
import com.github.filosganga.geogson.model.positions.SinglePosition;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Position {
private @Nullable ZonedDateTime updatedAt;
private @Nullable ZonedDateTime createdAt;
private @Nullable Geometry<SinglePosition> geometry;
private @Nullable Properties properties;
private @Nullable String type;
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable Geometry<SinglePosition> getGeometry() {
return geometry;
}
public @Nullable Properties getProperties() {
return properties;
}
public @Nullable String getType() {
return type;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("updatedAt", updatedAt).append("createdAt", createdAt)
.append("geometry", geometry).append("properties", properties).append("type", type).toString();
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Preconditionning {
private @Nullable AirConditioning airConditioning;
public @Nullable AirConditioning getAirConditioning() {
return airConditioning;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("airConditioning", airConditioning).toString();
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Privacy {
private @Nullable ZonedDateTime createdAt;
private @Nullable String state;
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable String getState() {
return state;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("createdAt", createdAt).append("state", state).toString();
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Program {
private @Nullable Occurence occurence;
private @Nullable String recurrence;
private @Nullable String start;
private @Nullable Boolean enabled;
private @Nullable BigDecimal slot;
public @Nullable Occurence getOccurence() {
return occurence;
}
public @Nullable String getRecurrence() {
return recurrence;
}
public @Nullable String getStart() {
return start;
}
public @Nullable Boolean isEnabled() {
return enabled;
}
public @Nullable BigDecimal getSlot() {
return slot;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("occurence", occurence).append("recurrence", recurrence)
.append("start", start).append("enabled", enabled).append("slot", slot).toString();
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Properties {
private @Nullable BigDecimal heading;
private @Nullable BigDecimal signalQuality;
private @Nullable String type;
public @Nullable BigDecimal getHeading() {
return heading;
}
public @Nullable BigDecimal getSignalQuality() {
return signalQuality;
}
public @Nullable String getType() {
return type;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("heading", heading).append("signalQuality", signalQuality)
.append("type", type).toString();
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Safety {
private @Nullable ZonedDateTime createdAt;
private @Nullable String beltWarning;
private @Nullable String eCallTriggeringRequest;
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable String getBeltWarning() {
return beltWarning;
}
public @Nullable String getECallTriggeringRequest() {
return eCallTriggeringRequest;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("createdAt", createdAt).append("beltWarning", beltWarning)
.append("eCallTriggeringRequest", eCallTriggeringRequest).toString();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Service {
private @Nullable String type;
private @Nullable ZonedDateTime updatedAt;
public @Nullable String getType() {
return type;
}
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("type", type).append("updatedAt", updatedAt).toString();
}
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
class ToStringBuilder implements Appendable, CharSequence {
protected StringBuilder stringBuilder = new StringBuilder();
ToStringBuilder(Object obj) {
}
@Override
public int length() {
return stringBuilder.length();
}
@Override
public char charAt(int index) {
return stringBuilder.charAt(index);
}
@Override
public CharSequence subSequence(int start, int end) {
return stringBuilder.subSequence(start, end);
}
@Override
public ToStringBuilder append(@Nullable CharSequence csq) throws IOException {
if (stringBuilder.length() != 0) {
stringBuilder.append(", ");
}
stringBuilder.append(csq);
return this;
}
@Override
public ToStringBuilder append(@Nullable CharSequence csq, int start, int end) throws IOException {
if (stringBuilder.length() != 0) {
stringBuilder.append(", ");
}
stringBuilder.append(csq, start, end);
return this;
}
@Override
public ToStringBuilder append(char c) throws IOException {
if (stringBuilder.length() != 0) {
stringBuilder.append(", ");
}
stringBuilder.append(c);
return this;
}
public ToStringBuilder append(Object key, @Nullable Object value) {
if (stringBuilder.length() != 0) {
stringBuilder.append(", ");
}
stringBuilder.append(key.toString() + ": " + ((value == null) ? "(null)" : value.toString()));
return this;
}
@Override
public String toString() {
return "{ " + stringBuilder.toString() + " }";
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class User {
private @Nullable String email;
private @Nullable String firstName;
private @Nullable String lastName;
@SerializedName("_embedded")
private @Nullable Embedded embedded;
private @Nullable ZonedDateTime createdAt;
private @Nullable ZonedDateTime updatedAt;
private static class Embedded {
private @Nullable List<Vehicle> vehicles;
@Override
public String toString() {
return new ToStringBuilder(this).append("vehicles", vehicles).toString();
}
}
public @Nullable String getEmail() {
return email;
}
public @Nullable String getLastName() {
return lastName;
}
public @Nullable String getFirstName() {
return firstName;
}
public @Nullable List<Vehicle> getVehicles() {
final Embedded resEmbedded = embedded;
if (resEmbedded != null) {
return resEmbedded.vehicles;
} else {
return null;
}
}
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("createdAt", createdAt).append("updatedAt", createdAt)
.append("email", email).append("firstName", firstName).append("lastName", lastName)
.append("vehicles", embedded != null ? embedded : null).toString();
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class Vehicle {
private @Nullable String id;
private @Nullable String vin;
private @Nullable String brand;
private @Nullable String label;
@SerializedName("engine")
private @Nullable List<Engine> engines;
private @Nullable ZonedDateTime createdAt;
private @Nullable ZonedDateTime updatedAt;
public @Nullable String getId() {
return id;
}
public @Nullable String getVin() {
return vin;
}
public @Nullable String getBrand() {
return brand;
}
public @Nullable String getLabel() {
return label;
}
public @Nullable List<Engine> getEngines() {
return engines;
}
public @Nullable ZonedDateTime getCreatedAt() {
return createdAt;
}
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("createdAt", createdAt).append("updatedAt", createdAt).append("id", id)
.append("vin", vin).append("brand", brand).append("label", label).append("engines", engines).toString();
}
}

View File

@@ -0,0 +1,148 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.api.dto;
import java.time.ZonedDateTime;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class VehicleStatus {
private @Nullable ZonedDateTime updatedAt;
@SerializedName("_embedded")
private @Nullable Embedded embedded;
private @Nullable Battery battery;
private @Nullable DoorsState doorsState;
private @Nullable List<Energy> energy = null;
private @Nullable Environment environment;
private @Nullable Ignition ignition;
private @Nullable Kinetic kinetic;
@SerializedName("timed.odometer")
private @Nullable Odometer odometer;
private @Nullable Position lastPosition;
private @Nullable Preconditionning preconditionning;
private @Nullable Privacy privacy;
private @Nullable Safety safety;
private @Nullable Service service;
private static class Embedded {
private @Nullable Extension extension;
@Override
public String toString() {
return new ToStringBuilder(this).append("extension", extension).toString();
}
}
private static class Extension {
private @Nullable Kinetic kinetic;
private @Nullable Odometer odometer;
@Override
public String toString() {
return new ToStringBuilder(this).append("kinetic", kinetic).append("odometer", odometer).toString();
}
}
public @Nullable Kinetic getKinetic() {
if (kinetic != null) {
return kinetic;
} else {
final Embedded finalEmbedded = embedded;
if (finalEmbedded != null) {
final Extension finalExtension = finalEmbedded.extension;
if (finalExtension != null) {
return finalExtension.kinetic;
}
}
return null;
}
}
public @Nullable Odometer getOdometer() {
if (odometer != null) {
return odometer;
} else {
Embedded finalEmbedded = embedded;
if (finalEmbedded != null) {
final Extension finalExtension = finalEmbedded.extension;
if (finalExtension != null) {
return finalExtension.odometer;
}
}
return null;
}
}
public @Nullable ZonedDateTime getUpdatedAt() {
return updatedAt;
}
public @Nullable Battery getBattery() {
return battery;
}
public @Nullable DoorsState getDoorsState() {
return doorsState;
}
public @Nullable List<Energy> getEnergy() {
return energy;
}
public @Nullable Environment getEnvironment() {
return environment;
}
public @Nullable Ignition getIgnition() {
return ignition;
}
public @Nullable Position getLastPosition() {
return lastPosition;
}
public @Nullable Preconditionning getPreconditionning() {
return preconditionning;
}
public @Nullable Privacy getPrivacy() {
return privacy;
}
public @Nullable Safety getSafety() {
return safety;
}
public @Nullable Service getService() {
return service;
}
@Override
public String toString() {
return new ToStringBuilder(this).append("updatedAt", updatedAt).append("_embedded", embedded)
.append("battery", battery).append("doorsState", doorsState).append("energy", energy)
.append("environment", environment).append("ignition", ignition).append("kinetic", kinetic)
.append("odometer", odometer).append("lastPosition", lastPosition)
.append("preconditionning", preconditionning).append("privacy", privacy).append("safety", safety)
.append("service", service).toString();
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.exceptions;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* An exception that occurred while communicating with an groupepsa or an groupepsa bridge
*
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class GroupePSACommunicationException extends IOException {
private static final long serialVersionUID = 1L;
private int statusCode = -1;
public GroupePSACommunicationException(Exception e) {
super(e);
}
public GroupePSACommunicationException(int statusCode, Exception e) {
super(e);
this.statusCode = statusCode;
}
public GroupePSACommunicationException(int statusCode) {
this.statusCode = statusCode;
}
public GroupePSACommunicationException(int statusCode, String message) {
super(message);
this.statusCode = statusCode;
}
public GroupePSACommunicationException(String message, Exception e) {
super(message, e);
}
public GroupePSACommunicationException(String message) {
super(message);
}
public int getStatusCode() {
return statusCode;
}
@Override
public @Nullable String getMessage() {
String message = super.getMessage();
return message == null ? null : "Rest call failed: statusCode=" + statusCode + ", message=" + message;
}
@Override
public String toString() {
return getClass().getSimpleName() + ": statusCode=" + statusCode + ", message=" + super.getMessage()
+ ", cause: " + getCause();
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class UnauthorizedException extends GroupePSACommunicationException {
private static final long serialVersionUID = 1L;
public UnauthorizedException(int statusCode, Exception e) {
super(statusCode, e);
}
public UnauthorizedException(int statusCode) {
super(statusCode);
}
public UnauthorizedException(int statusCode, String message) {
super(statusCode, message);
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.rest.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class UnavailableException extends GroupePSACommunicationException {
private static final long serialVersionUID = 1L;
public UnavailableException(int statusCode, Exception e) {
super(statusCode, e);
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.things;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link GroupePSAConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class GroupePSAConfiguration {
private @Nullable String id;
private @Nullable Integer pollingInterval;
private @Nullable Integer onlineInterval;
@Nullable
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public @Nullable Integer getPollingInterval() {
return pollingInterval;
}
public void setPollingInterval(Integer pollingInterval) {
this.pollingInterval = pollingInterval;
}
public @Nullable Integer getOnlineInterval() {
return onlineInterval;
}
public void setOnlineInterval(Integer onlineInterval) {
this.onlineInterval = onlineInterval;
}
}

View File

@@ -0,0 +1,479 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.things;
import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.*;
import java.math.BigDecimal;
import java.text.MessageFormat;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import javax.measure.Quantity;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.groupepsa.internal.bridge.GroupePSABridgeHandler;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Air;
import org.openhab.binding.groupepsa.internal.rest.api.dto.AirConditioning;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Battery;
import org.openhab.binding.groupepsa.internal.rest.api.dto.BatteryStatus;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Charging;
import org.openhab.binding.groupepsa.internal.rest.api.dto.DoorsState;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Energy;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Environment;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Health;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Ignition;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Kinetic;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Luminosity;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Odometer;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Opening;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Position;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Preconditionning;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Privacy;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Properties;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Safety;
import org.openhab.binding.groupepsa.internal.rest.api.dto.Service;
import org.openhab.binding.groupepsa.internal.rest.api.dto.VehicleStatus;
import org.openhab.binding.groupepsa.internal.rest.exceptions.GroupePSACommunicationException;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.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.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.filosganga.geogson.model.Geometry;
import com.github.filosganga.geogson.model.positions.SinglePosition;
/**
* The {@link GroupePSAHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class GroupePSAHandler extends BaseThingHandler {
private static final long DEFAULT_POLLING_INTERVAL_M = TimeUnit.MINUTES.toMinutes(1);
private static final long DEFAULT_ONLINE_INTERVAL_M = TimeUnit.MINUTES.toMinutes(60);
private final Logger logger = LoggerFactory.getLogger(GroupePSAHandler.class);
private @Nullable String id = null;
private long lastQueryTimeNs = 0L;
private @Nullable ScheduledFuture<?> groupepsaPollingJob;
private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1);
private long onlineIntervalM;
public GroupePSAHandler(Thing thing) {
super(thing);
}
@Override
protected @Nullable Bridge getBridge() {
return super.getBridge();
}
private void pollStatus() {
Bridge bridge = getBridge();
if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
updateGroupePSAState();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
};
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
refreshChannels(channelUID);
}
}
private void refreshChannels(ChannelUID channelUID) {
updateGroupePSAState();
}
@Override
public void initialize() {
if (getBridgeHandler() != null) {
GroupePSAConfiguration currentConfig = getConfigAs(GroupePSAConfiguration.class);
final String id = currentConfig.getId();
final Integer pollingIntervalM = currentConfig.getPollingInterval();
final Integer onlineIntervalM = currentConfig.getOnlineInterval();
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/conf-error-no-vehicle-id");
} else if (pollingIntervalM != null && pollingIntervalM < 1) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/conf-error-invalid-polling-interval");
} else if (onlineIntervalM != null && onlineIntervalM < 1) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/conf-error-invalid-online-interval");
} else {
this.id = id;
this.onlineIntervalM = onlineIntervalM != null ? onlineIntervalM : DEFAULT_ONLINE_INTERVAL_M;
startGroupePSAPolling(pollingIntervalM);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
}
@Nullable
public GroupePSABridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
if (bridge != null) {
ThingHandler handler = bridge.getHandler();
if (handler instanceof GroupePSABridgeHandler) {
return (GroupePSABridgeHandler) handler;
}
}
return null;
}
@Override
public void dispose() {
stopGroupePSAPolling();
id = null;
}
private void startGroupePSAPolling(@Nullable Integer pollingIntervalM) {
if (groupepsaPollingJob == null) {
final long pollingIntervalToUse = pollingIntervalM == null ? DEFAULT_POLLING_INTERVAL_M : pollingIntervalM;
groupepsaPollingJob = scheduler.scheduleWithFixedDelay(() -> pollStatus(), 1, pollingIntervalToUse * 60,
TimeUnit.SECONDS);
}
}
private void stopGroupePSAPolling() {
ScheduledFuture<?> job = groupepsaPollingJob;
if (job != null) {
job.cancel(true);
groupepsaPollingJob = null;
}
}
private boolean isValidResult(VehicleStatus vehicle) {
return vehicle.getUpdatedAt() != null;
}
private boolean isConnected(VehicleStatus vehicle) {
ZonedDateTime updatedAt = vehicle.getUpdatedAt();
if (updatedAt == null) {
return false;
}
return updatedAt.isAfter(ZonedDateTime.now().minusMinutes(onlineIntervalM));
}
private synchronized void updateGroupePSAState() {
if (System.nanoTime() - lastQueryTimeNs <= maxQueryFrequencyNanos) {
return;
}
lastQueryTimeNs = System.nanoTime();
String id = this.id;
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-vehicle-id");
return;
}
GroupePSABridgeHandler groupepsaBridge = getBridgeHandler();
if (groupepsaBridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
return;
}
try {
VehicleStatus vehicle = groupepsaBridge.getVehicleStatus(id);
if (vehicle != null && isValidResult(vehicle)) {
logger.trace("Vehicle: {}", vehicle.toString());
logger.debug("Update vehicle state now: {}, lastupdate: {}", ZonedDateTime.now(),
vehicle.getUpdatedAt());
updateChannelState(vehicle);
if (isConnected(vehicle)) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
"@text/comm-error-vehicle-not-connected-to-cloud");
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/comm-error-query-vehicle-failed");
}
} catch (GroupePSACommunicationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
MessageFormat.format("@text/comm-error-query-vehicle-failed", e.getMessage()));
}
}
private void updateChannelState(VehicleStatus vehicle) {
final DoorsState doorsState = vehicle.getDoorsState();
if (doorsState != null) {
buildDoorChannels(doorsState);
List<Opening> openings = doorsState.getOpening();
if (openings != null) {
for (Opening opening : openings) {
String id = opening.getIdentifier();
if (id != null) {
ChannelUID channelUID = new ChannelUID(getThing().getUID(), CHANNEL_GROUP_DOORS,
id.toLowerCase());
updateState(channelUID, "open".equalsIgnoreCase(opening.getState()) ? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
}
}
}
List<String> lockedState = doorsState.getLockedState();
updateState(CHANNEL_DOORS_LOCK, lockedState, x -> x.get(0));
} else {
updateState(CHANNEL_DOORS_LOCK, UnDefType.UNDEF);
}
updateState(CHANNEL_BATTERY_CURRENT, vehicle.getBattery(), Battery::getCurrent, Units.AMPERE);
updateState(CHANNEL_BATTERY_VOLTAGE, vehicle.getBattery(), Battery::getVoltage, Units.VOLT);
updateState(CHANNEL_ENVIRONMENT_TEMPERATURE, vehicle.getEnvironment(), Environment::getAir, Air::getTemp,
SIUnits.CELSIUS);
updateStateBoolean(CHANNEL_ENVIRONMENT_DAYTIME, vehicle.getEnvironment(), Environment::getLuminosity,
Luminosity::isDay);
updateState(CHANNEL_MOTION_IGNITION, vehicle.getIgnition(), Ignition::getType);
updateStateBoolean(CHANNEL_MOTION_MOVING, vehicle.getKinetic(), Kinetic::isMoving);
updateState(CHANNEL_MOTION_ACCELERATION, vehicle.getKinetic(), Kinetic::getAcceleration,
Units.METRE_PER_SQUARE_SECOND);
updateState(CHANNEL_MOTION_SPEED, vehicle.getKinetic(), Kinetic::getSpeed, SIUnits.KILOMETRE_PER_HOUR);
updateState(CHANNEL_MOTION_MILEAGE, vehicle.getOdometer(), Odometer::getMileage,
MetricPrefix.KILO(SIUnits.METRE));
Position lastPosition = vehicle.getLastPosition();
if (lastPosition != null) {
Geometry<SinglePosition> geometry = lastPosition.getGeometry();
if (geometry != null) {
SinglePosition position = (SinglePosition) geometry.positions();
if (Double.isFinite(position.alt())) {
updateState(CHANNEL_POSITION_POSITION, new PointType(new DecimalType(position.lat()),
new DecimalType(position.lon()), new DecimalType(position.alt())));
} else {
updateState(CHANNEL_POSITION_POSITION,
new PointType(new DecimalType(position.lat()), new DecimalType(position.lon())));
}
} else {
updateState(CHANNEL_POSITION_POSITION, UnDefType.UNDEF);
}
updateState(CHANNEL_POSITION_HEADING, lastPosition.getProperties(), Properties::getHeading,
Units.DEGREE_ANGLE);
updateState(CHANNEL_POSITION_TYPE, lastPosition.getProperties(), Properties::getType);
updateState(CHANNEL_POSITION_SIGNALSTRENGTH, lastPosition.getProperties(), Properties::getSignalQuality,
Units.PERCENT);
}
updateState(CHANNEL_VARIOUS_LAST_UPDATED, vehicle.getUpdatedAt());
updateState(CHANNEL_VARIOUS_PRIVACY, vehicle.getPrivacy(), Privacy::getState);
updateState(CHANNEL_VARIOUS_BELT, vehicle.getSafety(), Safety::getBeltWarning);
updateState(CHANNEL_VARIOUS_EMERGENCY, vehicle.getSafety(), Safety::getECallTriggeringRequest);
updateState(CHANNEL_VARIOUS_SERVICE, vehicle.getService(), Service::getType);
updateState(CHANNEL_VARIOUS_PRECONDITINING, vehicle.getPreconditionning(), Preconditionning::getAirConditioning,
AirConditioning::getStatus);
updateState(CHANNEL_VARIOUS_PRECONDITINING_FAILURE, vehicle.getPreconditionning(),
Preconditionning::getAirConditioning, AirConditioning::getFailureCause);
List<Energy> energies = vehicle.getEnergy();
if (energies != null) {
for (Energy energy : energies) {
if ("Fuel".equalsIgnoreCase(energy.getType())) {
updateState(CHANNEL_FUEL_AUTONOMY, energy, Energy::getAutonomy, MetricPrefix.KILO(SIUnits.METRE));
updateState(CHANNEL_FUEL_CONSUMPTION, energy, Energy::getConsumption,
Units.LITRE.divide(MetricPrefix.KILO(SIUnits.METRE)));
updateState(CHANNEL_FUEL_LEVEL, energy, Energy::getLevel, Units.PERCENT);
} else if ("Electric".equalsIgnoreCase(energy.getType())) {
updateState(CHANNEL_ELECTRIC_AUTONOMY, energy, Energy::getAutonomy,
MetricPrefix.KILO(SIUnits.METRE));
updateState(CHANNEL_ELECTRIC_RESIDUAL, energy, Energy::getResidual, Units.KILOWATT_HOUR);
updateState(CHANNEL_ELECTRIC_LEVEL, energy, Energy::getLevel, Units.PERCENT);
updateState(CHANNEL_ELECTRIC_BATTERY_CAPACITY, energy, Energy::getBattery,
BatteryStatus::getCapacity, Units.KILOWATT_HOUR);
updateState(CHANNEL_ELECTRIC_BATTERY_HEALTH_CAPACITY, energy, Energy::getBattery,
BatteryStatus::getHealth, Health::getCapacity, Units.PERCENT);
updateState(CHANNEL_ELECTRIC_BATTERY_HEALTH_RESISTANCE, energy, Energy::getBattery,
BatteryStatus::getHealth, Health::getResistance, Units.PERCENT);
updateState(CHANNEL_ELECTRIC_CHARGING_STATUS, energy, Energy::getCharging, Charging::getStatus);
updateState(CHANNEL_ELECTRIC_CHARGING_MODE, energy, Energy::getCharging, Charging::getChargingMode);
updateStateBoolean(CHANNEL_ELECTRIC_CHARGING_PLUGGED, energy, Energy::getCharging,
Charging::isPlugged);
updateState(CHANNEL_ELECTRIC_CHARGING_RATE, energy, Energy::getCharging, Charging::getChargingRate,
SIUnits.KILOMETRE_PER_HOUR);
updateState(CHANNEL_ELECTRIC_CHARGING_REMAININGTIME, energy, Energy::getCharging,
Charging::getRemainingTime, x -> new BigDecimal(x.getSeconds()), Units.SECOND);
updateState(CHANNEL_ELECTRIC_CHARGING_NEXTDELAYEDTIME, energy, Energy::getCharging,
Charging::getNextDelayedTime, x -> new BigDecimal(x.getSeconds()), Units.SECOND);
}
}
}
}
void buildDoorChannels(final DoorsState doorsState) {
ThingHandlerCallback callback = getCallback();
if (callback == null) {
return;
}
ThingBuilder thingBuilder = editThing();
List<Channel> channels = getThing().getChannelsOfGroup(CHANNEL_GROUP_DOORS);
thingBuilder.withoutChannels(channels);
ChannelUID channelUID = new ChannelUID(getThing().getUID(), CHANNEL_DOORS_LOCK);
ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_DOORLOCK);
thingBuilder.withChannel(callback.createChannelBuilder(channelUID, channelTypeUID).build());
List<Opening> openings = doorsState.getOpening();
if (openings != null) {
for (Opening opening : openings) {
String id = opening.getIdentifier();
if (id != null) {
channelUID = new ChannelUID(getThing().getUID(), CHANNEL_GROUP_DOORS, id.toLowerCase());
channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_DOOROPEN);
thingBuilder.withChannel(callback.createChannelBuilder(channelUID, channelTypeUID).build());
}
}
}
updateThing(thingBuilder.build());
}
// Various update helper functions
protected <T extends Quantity<T>> void updateState(String channelID, @Nullable BigDecimal number, Unit<T> unit) {
if (number != null) {
updateState(channelID, new QuantityType<T>(number, unit));
} else {
updateState(channelID, UnDefType.UNDEF);
}
}
protected <T1, T2 extends Quantity<T2>> void updateState(String channelID, final @Nullable T1 object,
Function<? super T1, @Nullable BigDecimal> mapper, Unit<T2> unit) {
updateState(channelID, object != null ? mapper.apply(object) : null, unit);
}
protected <T1, T2, T3 extends Quantity<T3>> void updateState(String channelID, final @Nullable T1 object1,
Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable BigDecimal> mapper2,
Unit<T3> unit) {
final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
updateState(channelID, object2 != null ? mapper2.apply(object2) : null, unit);
}
protected <T1, T2, T3, T4 extends Quantity<T4>> void updateState(String channelID, final @Nullable T1 object1,
Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable T3> mapper2,
Function<? super T3, @Nullable BigDecimal> mapper3, Unit<T4> unit) {
final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
final @Nullable T3 object3 = object2 != null ? mapper2.apply(object2) : null;
updateState(channelID, object3 != null ? mapper3.apply(object3) : null, unit);
}
protected void updateState(String channelID, @Nullable ZonedDateTime date) {
if (date != null) {
updateState(channelID, new DateTimeType(date));
} else {
updateState(channelID, UnDefType.UNDEF);
}
}
protected <T1> void updateStateDate(String channelID, @Nullable T1 object,
Function<? super T1, @Nullable ZonedDateTime> mapper) {
updateState(channelID, object != null ? mapper.apply(object) : null);
}
protected <T1, T2> void updateStateDate(String channelID, @Nullable T1 object1,
Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable ZonedDateTime> mapper2) {
final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
}
protected void updateState(String channelID, @Nullable String text) {
if (text != null) {
updateState(channelID, new StringType(text));
} else {
updateState(channelID, UnDefType.UNDEF);
}
}
protected <T1> void updateState(String channelID, @Nullable T1 object,
Function<? super T1, @Nullable String> mapper) {
updateState(channelID, object != null ? mapper.apply(object) : null);
}
protected <T1, T2> void updateState(String channelID, @Nullable T1 object1,
Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable String> mapper2) {
final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
}
protected void updateState(String channelID, @Nullable Boolean value) {
if (value != null) {
updateState(channelID, value ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
} else {
updateState(channelID, UnDefType.UNDEF);
}
}
protected <T1> void updateStateBoolean(String channelID, @Nullable T1 object,
Function<? super T1, @Nullable Boolean> mapper) {
updateState(channelID, object != null ? mapper.apply(object) : null);
}
protected <T1, T2> void updateStateBoolean(String channelID, final @Nullable T1 object1,
Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable Boolean> mapper2) {
final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="groupepsa" 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>Groupe PSA Binding</name>
<description>Provides information and (limited) control of cars from the Groupe PSA (Peugeot, Citroen, Opel, etc.).</description>
</binding:binding>

View File

@@ -0,0 +1,21 @@
# binding
binding.groupepsa.name = Groupe PSA Binding
binding.groupepsa.description = Provides information and (limited) control of cars from the Groupe PSA (Peugeot, Citroen, Opel, etc.).
#configuration errors
conf-error-no-vendor = Cannot connect to Groupe PSA bridge as no vendor is available in the configuration
conf-error-unknown-vendor = Cannot connect to Groupe PSA bridge as the vendor is not valid
conf-error-no-username = Cannot connect to Groupe PSA bridge as no username is available in the configuration
conf-error-no-password = Cannot connect to Groupe PSA bridge as no password is available in the configuration
conf-error-no-clientid = Cannot connect to Groupe PSA bridge as no client ID is available in the configuration
conf-error-no-clientsecret = Cannot connect to Groupe PSA bridge as no client secret is available in the configuration
conf-error-invalid-polling-interval = Invalid polling interval specified. The interval has to be >= 1
conf-error-invalid-online-interval = Invalid online check interval specified. The interval has to be >= 1
conf-error-no-vehicle-id = No VIN specified. Unable to communicate with the vehicle without an ID
conf-error-no-bridge = No valid bridge for the vehicle is available
#communication errors
comm-error-vehicle-not-connected-to-cloud = Vehicle not connected to the cloud
comm-error-query-vehicle-failed = Unable to query the vehicle status ({0})
comm-error-query-vehicles-failed = Unable to get a list of vehicles ({0})

View File

@@ -0,0 +1,535 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="groupepsa"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="bridge">
<label>Groupe PSA Bridge</label>
<description>The bridge to communicate with the PSA (Peugeot, Citroen, Vauxhall, Opel, DS) Web API for End-Users</description>
<category>WebService</category>
<representation-property>vendor</representation-property>
<config-description>
<parameter name="vendor" type="text" required="true">
<label>Brand</label>
<description>The brand of the car</description>
<options>
<option value="PEUGEOT">Peugeot</option>
<option value="CITROEN">Citroen</option>
<option value="DS">DS</option>
<option value="OPEL">Opel</option>
<option value="VAUXHALL">Vauxhall</option>
</options>
</parameter>
<parameter name="userName" type="text" required="true">
<label>User Name</label>
<description>The user name for the mypeugot/mycitroen/myds/myopel/myvauxhall website or app</description>
</parameter>
<parameter name="password" type="text" required="true">
<context>password</context>
<description>The password for the given user</description>
</parameter>
<parameter name="pollingInterval" type="integer" required="false" unit="min">
<label>Polling Interval</label>
<default>60</default>
<description>How often the available vehicles should be queried in minutes</description>
</parameter>
<parameter name="clientId" type="text" required="true">
<advanced>true</advanced>
<default>07364655-93cb-4194-8158-6b035ac2c24c</default>
<label>Client ID</label>
<description>The client ID for API access: can normally left at the default value. (see:
https://developer.groupe-psa.io/webapi/b2c/quickstart/connect/#article)</description>
</parameter>
<parameter name="clientSecret" type="text" required="true">
<advanced>true</advanced>
<default>F2kK7lC5kF5qN7tM0wT8kE3cW1dP0wC5pI6vC0sQ5iP5cN8cJ8</default>
<label>Client Secret</label>
<description>The client secret for API access: can normally left at the default value. (see:
https://developer.groupe-psa.io/webapi/b2c/quickstart/connect/#article)</description>
</parameter>
</config-description>
</bridge-type>
<thing-type id="vehicle">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Groupe PSA Car</label>
<description>Car connected via Groupe PSA (Peugeot, Citroen, Vauxhall, Opel, DS) Bridge</description>
<channel-groups>
<channel-group id="battery" typeId="battery"/>
<channel-group id="doors" typeId="doors"/>
<channel-group id="environment" typeId="environment"/>
<channel-group id="motion" typeId="motion"/>
<channel-group id="position" typeId="position"/>
<channel-group id="various" typeId="various"/>
<channel-group id="fuel" typeId="fuel"/>
<channel-group id="electric" typeId="electric"/>
</channel-groups>
<properties>
<property name="vendor">N/A</property>
<property name="model">N/A</property>
<property name="vin">N/A</property>
</properties>
<representation-property>id</representation-property>
<config-description>
<parameter name="id" type="text" required="true">
<label>ID</label>
<description>Vehicle API ID (obtained automatically during discovery, not equal to the VIN)</description>
</parameter>
<parameter name="pollingInterval" type="integer" required="false" unit="min">
<label>Polling Interval</label>
<default>5</default>
<description>How often this vehicle should be polled for updated information</description>
</parameter>
<parameter name="onlineInterval" type="integer" required="false" unit="min">
<label>Online Timeout</label>
<default>15</default>
<description>Timeout when the vehicle is considered offline</description>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="battery">
<label>Battery</label>
<description>Main battery</description>
<category>Energy</category>
<channels>
<channel id="current" typeId="current"/>
<channel id="voltage" typeId="level">
<label>Battery Level</label>
<description>Battery level</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="doors">
<label>Doors</label>
<description>State of the doors</description>
<category>Door</category>
<channels>
<channel id="locked" typeId="doorLock"/>
<channel id="driver" typeId="doorOpen">
<label>Driver Door</label>
<description>Driver door open state</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="environment">
<label>Environment</label>
<description>Environmental conditions</description>
<channels>
<channel id="temperature" typeId="temperature">
<label>Outside Temperature</label>
<description>Outside Temperature</description>
</channel>
<channel id="daytime" typeId="daytime"/>
</channels>
</channel-group-type>
<channel-group-type id="motion">
<label>Motion</label>
<description>Motion information</description>
<channels>
<channel id="ignition" typeId="ignition"/>
<channel id="acceleration" typeId="acceleration"/>
<channel id="moving" typeId="moving"/>
<channel id="speed" typeId="speed"/>
<channel id="mileage" typeId="mileage"/>
</channels>
</channel-group-type>
<channel-group-type id="position">
<label>Position</label>
<description>Location information</description>
<channels>
<channel id="position" typeId="position"/>
<channel id="heading" typeId="heading"/>
<channel id="type" typeId="type"/>
<channel id="signal" typeId="system.signal-strength"/>
</channels>
</channel-group-type>
<channel-group-type id="various">
<label>Various</label>
<description>Miscellaneous information</description>
<channels>
<channel id="lastUpdated" typeId="lastUpdated"/>
<channel id="privacy" typeId="privacy"/>
<channel id="belt" typeId="belt"/>
<channel id="emergency" typeId="emergency"/>
<channel id="service" typeId="service"/>
<channel id="preconditioning" typeId="preconditioning"/>
<channel id="preconditioningFailure" typeId="preconditioningFailure"/>
</channels>
</channel-group-type>
<channel-type id="current">
<item-type>Number:ElectricCurrent</item-type>
<label>Current</label>
<description>Electrical current</description>
<category>Energy</category>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="voltage">
<item-type>Number:ElectricPotential</item-type>
<label>Voltage</label>
<description>Voltage</description>
<category>Energy</category>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Temperature</description>
<category>Temperature</category>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="daytime">
<item-type>Contact</item-type>
<label>Daytime</label>
<description>Enabled if it is daytime</description>
</channel-type>
<channel-type id="doorLock">
<item-type>String</item-type>
<label>Door Lock</label>
<description>Door lock state</description>
<category>Door</category>
<state readOnly="true">
<options>
<option value="Unlocked">Unlocked</option>
<option value="Locked">Locked</option>
<option value="SuperLocked">Super Locked</option>
<option value="DriverDoorUnlocked">Driver Door Unlocked</option>
<option value="CabinDoorsUnlocked">Cabin Doors Unlocked</option>
<option value="CargoDoorsLocked">Cargo Doors Locked</option>
<option value="CargoDoorsUnlocked">Cargo Doors Unlocked</option>
<option value="RearDoorsLocked">Rear Doors Locked</option>
<option value="RearDoorsUnlocked">Rear Doors Unlocked</option>
</options>
</state>
</channel-type>
<channel-type id="doorOpen">
<item-type>Contact</item-type>
<label>Door Open</label>
<description>Door is open</description>
<category>Door</category>
</channel-type>
<channel-type id="ignition">
<item-type>String</item-type>
<label>Ignition</label>
<description>Ignition state</description>
<state readOnly="true">
<options>
<option value="Stop">Stop</option>
<option value="StartUp">Start up</option>
<option value="Start">Start</option>
<option value="Free">Free</option>
</options>
</state>
</channel-type>
<channel-type id="moving">
<item-type>Contact</item-type>
<label>Moving</label>
<description>Vehicle is moving</description>
</channel-type>
<channel-type id="acceleration">
<item-type>Number:Acceleration</item-type>
<label>Acceleration</label>
<description>Current acceleration</description>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="speed">
<item-type>Number:Speed</item-type>
<label>Speed</label>
<description>Current speed</description>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="mileage">
<item-type>Number:Length</item-type>
<label>Mileage</label>
<description>Total travelled distance</description>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="position">
<item-type>Location</item-type>
<label>Position</label>
<description>Last known position</description>
<state pattern="%2$s°N %3$s°E %1$sm"/>
</channel-type>
<channel-type id="heading">
<item-type>Number:Angle</item-type>
<label>Heading</label>
<description>Direction of travel</description>
<state pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="type">
<item-type>String</item-type>
<label>Position Type</label>
<description>Position acquisition type</description>
<state readOnly="true">
<options>
<option value="Estimate">Estimate</option>
<option value="Acquire">Acquire</option>
</options>
</state>
</channel-type>
<channel-type id="lastUpdated">
<item-type>DateTime</item-type>
<label>Time of Last Update</label>
<description>Last time the results were updated on the server</description>
<category>Time</category>
</channel-type>
<channel-type id="privacy">
<item-type>String</item-type>
<label>Privacy Status</label>
<description>Privacy status</description>
<state readOnly="true">
<options>
<option value="None">None</option>
<option value="Geolocation">Location</option>
<option value="Full">Full</option>
</options>
</state>
</channel-type>
<channel-type id="belt">
<item-type>String</item-type>
<label>Belt Status</label>
<description>Seat belt status</description>
<state readOnly="true">
<options>
<option value="Normal">Belts on</option>
<option value="Omission">Belt open</option>
</options>
</state>
</channel-type>
<channel-type id="emergency">
<item-type>String</item-type>
<label>Emergency Call</label>
<description>Emergency call status</description>
<state readOnly="true">
<options>
<option value="NoRequest">None</option>
<option value="AirbagUnabled">Airbag Deployed</option>
<option value="Requested">Emergency Call Requested</option>
</options>
</state>
</channel-type>
<channel-type id="service">
<item-type>String</item-type>
<label>Service</label>
<description>Service Type</description>
<state readOnly="true">
<options>
<option value="Electric">Electric</option>
<option value="Hybrid">Hybrid</option>
<option value="Unknown">Unknown</option>
</options>
</state>
</channel-type>
<channel-type id="preconditioning">
<item-type>String</item-type>
<label>Air Conditioning</label>
<description>Air conditioning status</description>
<state readOnly="true">
<options>
<option value="Enabled">Enabled</option>
<option value="Disabled">Disabled</option>
<option value="Finished">Finished</option>
<option value="Failure">Failure</option>
</options>
</state>
</channel-type>
<channel-type id="preconditioningFailure">
<item-type>String</item-type>
<label>Air Conditioning Failure</label>
<description>Air conditioning failure cause</description>
<state readOnly="true">
<options>
<option value="Defect">Defect</option>
<option value="DoorOpened">Door Opened</option>
<option value="LowBattery">Low Battery</option>
<option value="LowFuelLevel">Low Fuel Level</option>
<option value="TooManyUnusedProg">Too many times unused</option>
</options>
</state>
</channel-type>
<channel-group-type id="fuel">
<label>Fuel</label>
<description>Thermic Motor Fuel Status</description>
<category>Energy</category>
<channels>
<channel id="autonomy" typeId="autonomy"/>
<channel id="consumption" typeId="consumption"/>
<channel id="level" typeId="level"/>
</channels>
</channel-group-type>
<channel-group-type id="electric">
<label>Electric Energy</label>
<description>Electric motor energy status</description>
<channels>
<channel id="autonomy" typeId="autonomy">
<label>Electric Autonomy</label>
<description>Remaining distance</description>
</channel>
<channel id="level" typeId="level">
<label>Electricity Level</label>
<description>Electricity level</description>
</channel>
<channel id="residual" typeId="residual"/>
<channel id="batteryCapacity" typeId="capacity"/>
<channel id="batteryHealthCapacity" typeId="healthCapacity"/>
<channel id="batteryHealthResistance" typeId="healthResistance"/>
<channel id="chargingStatus" typeId="chargingStatus"/>
<channel id="chargingMode" typeId="chargingMode"/>
<channel id="chargingPlugged" typeId="chargingPlugged"/>
<channel id="chargingRate" typeId="chargingRate"/>
<channel id="chargingRemainingTime" typeId="chargingRemainingTime"/>
<channel id="chargingNextDelayedTime" typeId="chargingNextDelayedTime"/>
</channels>
</channel-group-type>
<channel-type id="level">
<item-type>Number:Dimensionless</item-type>
<label>Fuel Level</label>
<description>Fuel level</description>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="autonomy">
<item-type>Number:Length</item-type>
<label>Fuel Autonomy</label>
<description>Remaining distance</description>
<state pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="consumption">
<item-type>Number:VolumetricFlowRate</item-type>
<label>Fuel Consumption</label>
<description>Fuel consumption</description>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="residual">
<item-type>Number:Energy</item-type>
<label>Remaining Charge</label>
<description>Remaining battery charge</description>
<category>BatteryLevel</category>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="capacity">
<item-type>Number:Energy</item-type>
<label>Battery Capacity</label>
<description>Battery capacity</description>
<category>BatteryLevel</category>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="healthCapacity">
<item-type>Number:Dimensionless</item-type>
<label>Battery Health - Capacity</label>
<description>Health of the battery capacity</description>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="healthResistance">
<item-type>Number:Dimensionless</item-type>
<label>Battery Health - Resistance</label>
<description>Health of the battery resistance</description>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="chargingStatus">
<item-type>String</item-type>
<label>Charging Status</label>
<description>Battery charging status</description>
<state readOnly="true">
<options>
<option value="Disconnected">Disconnected</option>
<option value="InProgess">In Progress</option>
<option value="Failure">Failure</option>
<option value="Stopped">Stopped</option>
<option value="Finished">Finished</option>
</options>
</state>
</channel-type>
<channel-type id="chargingMode">
<item-type>String</item-type>
<label>Charging Mode</label>
<description>Battery charging mode</description>
<state readOnly="true">
<options>
<option value="No">No</option>
<option value="Slow">Slow</option>
<option value="Quick">Fast</option>
</options>
</state>
</channel-type>
<channel-type id="chargingPlugged">
<item-type>Contact</item-type>
<label>Plugged In</label>
<description>Vehicle plugged in to charger</description>
</channel-type>
<channel-type id="chargingRate">
<item-type>Number:Speed</item-type>
<label>Charging Rate</label>
<description>Battery Charging Rate</description>
<category>BatteryLevel</category>
<state pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="chargingRemainingTime">
<item-type>Number:Time</item-type>
<label>Charging Time Remaining</label>
<description>Time remaining till charged</description>
<category>Time</category>
<state pattern="%tT"/>
</channel-type>
<channel-type id="chargingNextDelayedTime">
<item-type>Number:Time</item-type>
<label>Time Till Next Charging</label>
<description>Time till the next charging starts</description>
<category>Time</category>
<state pattern="%tT"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,159 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.groupepsa.internal.things;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpContentResponse;
import org.eclipse.jetty.client.HttpResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.openhab.binding.groupepsa.internal.bridge.GroupePSABridgeHandler;
import org.openhab.binding.groupepsa.internal.rest.api.GroupePSAConnectApi;
import org.openhab.binding.groupepsa.internal.rest.exceptions.GroupePSACommunicationException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.types.State;
/**
* The {@link GroupePSAHandlerTest} is responsible for testing the binding
*
* @author Arjan Mels - Initial contribution
*/
@NonNullByDefault
public class GroupePSAHandlerTest {
private @NonNullByDefault({}) AutoCloseable closeable;
private @NonNullByDefault({}) GroupePSAConnectApi api;
private @NonNullByDefault({}) GroupePSABridgeHandler bridgeHandler;
private @NonNullByDefault({}) GroupePSAHandler thingHandler;
private @NonNullByDefault({}) @Mock ThingHandlerCallback thingCallback;
private @NonNullByDefault({}) @Mock ThingHandlerCallback bridgeCallback;
private @NonNullByDefault({}) @Mock Thing thing;
private @NonNullByDefault({}) @Mock Bridge bridge;
private @NonNullByDefault({}) @Mock OAuthFactory oAuthFactory;
private @NonNullByDefault({}) @Mock HttpClient httpClient;
static String getResourceFileAsString(String fileName) throws GroupePSACommunicationException {
try (InputStream is = GroupePSAConnectApi.class.getResourceAsStream(fileName)) {
try (InputStreamReader isr = new InputStreamReader(is); BufferedReader reader = new BufferedReader(isr)) {
return reader.lines().collect(Collectors.joining(System.lineSeparator()));
}
} catch (Exception e) {
throw new GroupePSACommunicationException(e);
}
}
static HttpContentResponse createHttpResponse(String file) throws GroupePSACommunicationException {
return new HttpContentResponse(new HttpResponse(null, null).status(200),
getResourceFileAsString("/" + file).getBytes(), "json", "UTF-8");
}
@BeforeEach
@SuppressWarnings("null")
public void setUp() throws GroupePSACommunicationException {
closeable = MockitoAnnotations.openMocks(this);
// Create real objects
bridgeHandler = spy(new GroupePSABridgeHandler(bridge, oAuthFactory, httpClient));
thingHandler = spy(new GroupePSAHandler(thing));
api = spy(new GroupePSAConnectApi(httpClient, bridgeHandler, "clientId", "realm"));
// Setup API mock
doReturn(createHttpResponse("dummy_user.json")).when(api).executeRequest(contains("user"), anyString());
doReturn(createHttpResponse("dummy_vehiclestatus3.json")).when(api).executeRequest(contains("status"),
anyString());
// Setup bridge handler mock
bridgeHandler.setCallback(bridgeCallback);
doReturn(api).when(bridgeHandler).getAPI();
// Setup bridge mock
Configuration bridgeConfig = new Configuration();
bridgeConfig.put("vendor", "OPEL");
bridgeConfig.put("userName", "user");
bridgeConfig.put("password", "pwd");
bridgeConfig.put("clientId", "clientIdValue");
bridgeConfig.put("clientSecret", "clientSecretValue");
doReturn(bridgeConfig).when(bridge).getConfiguration();
doReturn(ThingStatus.ONLINE).when(bridge).getStatus();
doReturn(bridgeHandler).when(bridge).getHandler();
doReturn(new ThingUID("a:b:c")).when(bridge).getUID();
// Setup thing mock
Configuration thingConfig = new Configuration();
thingConfig.put("id", "mock_id");
doReturn(thingConfig).when(thing).getConfiguration();
doReturn(new ThingUID("a:b:c")).when(thing).getUID();
// Setup thing handler mock
thingHandler.setCallback(thingCallback);
doReturn(bridge).when(thingHandler).getBridge();
doNothing().when(thingHandler).buildDoorChannels(any());
}
@AfterEach
public void tearDown() throws Exception {
// Free any resources, like open database connections, files etc.
thingHandler.dispose();
bridgeHandler.dispose();
closeable.close();
}
@Test
public void intializeAndCheckChannels() throws InterruptedException {
// Initialize the bridge
bridgeHandler.initialize();
// check that the bridge is online
verify(bridgeCallback, timeout(2000)).statusUpdated(eq(bridge),
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
// Initialize the thing
thingHandler.initialize();
// check that the thing is offline without detail (last update time is not
// within 15 minutes)
verify(thingCallback, timeout(2000)).statusUpdated(eq(thing),
argThat(arg -> arg.getStatus().equals(ThingStatus.OFFLINE)
&& arg.getStatusDetail().equals(ThingStatusDetail.NONE)));
// check that the channels are updated
verify(thingCallback, atLeast(30)).stateUpdated(any(ChannelUID.class), any(State.class));
verify(thingCallback).stateUpdated(eq(new ChannelUID("a:b:c:electric#chargingStatus")),
eq(new StringType("Disconnected")));
verify(thingCallback).stateUpdated(eq(new ChannelUID("a:b:c:various#lastUpdated")), any(DateTimeType.class));
}
}

View File

@@ -0,0 +1,71 @@
{
"_links": {
"self": {
"href": "ivuvawoj"
},
"trips": {
"href": "ugihub"
},
"lastTrip": {
"href": "culugakin"
},
"lastEcodriving": {
"href": "bagnu"
}
},
"id": "7000650216898560",
"email": "iwni@georure.kr",
"firstName": "Floyd",
"lastName": "Gilbert",
"createdAt": "2019-08-24T14:15:22Z",
"updatedAt": "2019-08-24T14:15:22Z",
"_embedded": {
"vehicles": [
{
"_links": {
"self": {
"href": "ovoha"
},
"lastTrip": {
"href": "lotudwe"
},
"lastPosition": {
"href": "bobediirewelojbu"
},
"trips": {
"href": "vacofgutv"
},
"collisions": {
"href": "koafeu"
},
"telemetry": {
"href": "ewahzonug"
},
"maintenance": {
"href": "dovuzibwudmicel"
},
"alerts": {
"href": "izbisbiwpejunun"
},
"events": {
"href": "tece"
}
},
"id": "4336155157856256",
"vin": "mycar_vin1",
"brand": "opel",
"engine": [
{
"class": "Thermic",
"energy": "GPL"
},
{
"class": "Electric"
}
],
"createdAt": "2019-08-24T14:15:22Z",
"updatedAt": "2019-08-24T14:15:22Z"
}
]
}
}

View File

@@ -0,0 +1,89 @@
{
"lastPosition": {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
2.2221432,
48.772476,
172
]
},
"properties": {
"updatedAt": "2019-08-24T14:15:22Z",
"type": "Aquire",
"signalQuality": 6
}
},
"preconditionning": {
"airConditioning": {
"updatedAt": "2019-08-24T14:15:22Z",
"status": "Disabled",
"programs": [
{
"enabled": false,
"recurrence": "Daily",
"start": "PT0S"
},
{
"enabled": false,
"recurrence": "Daily",
"start": "PT0S"
},
{
"enabled": false,
"recurrence": "Daily",
"start": "PT0S"
},
{
"enabled": false,
"recurrence": "Daily",
"start": "PT0S"
}
]
}
},
"energy": [
{
"type": "Electric",
"level": 0,
"autonomy": 0,
"residual": 0
},
{
"type": "Fuel",
"level": 0,
"residual": 0
}
],
"createdAt": "2019-08-24T14:15:22Z",
"autonomy": 2000,
"ignition": {
"createdAt": "2019-08-24T14:15:22Z"
},
"vin": "VF3A12BCDEF3456",
"privacy": {
"createdAt": "2019-08-24T14:15:22Z",
"state": "None"
},
"battery": {
"voltage": 0.55,
"current": 0.55,
"createdAt": "2019-08-24T14:15:22Z"
},
"kinetic": {
"createdAt": "2019-08-24T14:15:22Z",
"acceleration": 0.55,
"speed": 0.55,
"pace": 0.55,
"moving": false
},
"_links": {
"self": {
"href": "https://api.groupe-psa.com/connectedcar/v4/user/vehicles/{id}/status"
},
"vehicles": {
"href": "https://api.groupe-psa.com/connectedcar/v4/user/vehicles/{id}"
}
}
}

View File

@@ -0,0 +1,180 @@
{
"createdAt": "2019-08-24T14:15:22Z",
"_embedded": {
"extension": {
"kinetic": {
"createdAt": "2019-08-24T14:15:22Z",
"acceleration": 0,
"moving": true,
"pace": 0,
"speed": 0
},
"odometer": {
"createdAt": "2019-08-24T14:15:22Z",
"mileage": 0
}
}
},
"_links": {
"self": {
"deprecation": "string",
"href": "string",
"hreflang": "string",
"name": "string",
"profile": "string",
"templated": true,
"title": "string",
"type": "string"
},
"vehicle": {
"deprecation": "string",
"href": "string",
"hreflang": "string",
"name": "string",
"profile": "string",
"templated": true,
"title": "string",
"type": "string"
}
},
"battery": {
"createdAt": "2019-08-24T14:15:22Z",
"current": 0,
"voltage": 0
},
"doorsState": {
"lockedState": [
"Unlocked"
],
"opening": [
{
"identifier": "Driver",
"state": "Open"
}
],
"updatedAt": "2019-08-24T14:15:22Z"
},
"energy": [
{
"updatedAt": "2019-08-24T14:15:22Z",
"createdAt": "2019-08-24T14:15:22Z",
"autonomy": 1,
"battery": {
"capacity": 0,
"health": {
"capacity": 0,
"resistance": 0
}
},
"charging": {
"chargingMode": "No",
"chargingRate": 0,
"nextDelayedTime": "PT15M",
"plugged": true,
"remainingTime": "PT15M",
"status": "Disconnected"
},
"consumption": 0,
"level": 0,
"residual": 0,
"type": "Fuel"
},
{
"updatedAt": "2019-08-24T14:15:22Z",
"createdAt": "2019-08-24T14:15:22Z",
"autonomy": 2,
"battery": {
"capacity": 0,
"health": {
"capacity": 0,
"resistance": 0
}
},
"charging": {
"chargingMode": "No",
"chargingRate": 0,
"nextDelayedTime": "PT15M",
"plugged": true,
"remainingTime": "PT15M",
"status": "Disconnected"
},
"consumption": 0,
"level": 0,
"residual": 0,
"type": "Electric"
}
],
"environment": {
"updatedAt": "2019-08-24T14:15:22Z",
"createdAt": "2019-08-24T14:15:22Z",
"air": {
"temp": 0
},
"luminosity": {
"day": true
}
},
"ignition": {
"updatedAt": "2019-08-24T14:15:22Z",
"type": "Stop"
},
"kinetic": {
"createdAt": "2019-08-24T14:15:22Z",
"acceleration": 0,
"moving": true,
"pace": 0,
"speed": 0
},
"lastPosition": {
"updatedAt": "2019-08-24T14:15:22Z",
"createdAt": "2019-08-24T14:15:22Z",
"geometry": {
"coordinates": [
0, 0, 0
],
"type": "Point"
},
"properties": {
"heading": 0,
"signalQuality": 0,
"type": "Estimate"
},
"type": "Feature"
},
"preconditionning": {
"airConditioning": {
"failureCause": "Defect",
"programs": [
{
"occurence": {
"day": [
"Mon"
],
"week": [
"string"
]
},
"recurrence": "Daily",
"start": "string",
"enabled": true,
"slot": 0
}
],
"status": "Enabled",
"updatedAt": "2019-08-24T14:15:22Z"
}
},
"privacy": {
"createdAt": "2019-08-24T14:15:22Z",
"state": "None"
},
"safety": {
"createdAt": "2019-08-24T14:15:22Z",
"beltWarning": "Normal",
"eCallTriggeringRequest": "AirbagUnabled"
},
"service": {
"type": "Electric",
"updatedAt": "2019-08-24T14:15:22Z"
}
}

View File

@@ -0,0 +1,102 @@
{
"lastPosition": {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
22.1111111,
11.2222222
]
},
"properties": {
"updatedAt": "2020-12-25T15:16:48Z",
"heading": 331,
"type": "Estimated"
}
},
"preconditionning": {
"airConditioning": {
"updatedAt": "2020-12-23T16:54:14Z",
"status": "Disabled",
"programs": [
{
"enabled": false,
"slot": 1,
"recurrence": "Daily",
"start": "PT6H15M",
"occurence": {
"day": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
]
}
},
{
"enabled": false,
"slot": 2,
"recurrence": "Daily",
"start": "PT8H",
"occurence": {
"day": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
]
}
}
]
}
},
"energy": [
{
"updatedAt": "2020-12-23T16:54:13Z",
"type": "Electric",
"level": 77,
"autonomy": 200,
"charging": {
"plugged": false,
"status": "Disconnected",
"remainingTime": "PT0S",
"chargingRate": 0,
"chargingMode": "No",
"nextDelayedTime": "PT5H"
}
}
],
"createdAt": "2020-12-25T15:16:48Z",
"battery": {
"voltage": 87,
"current": 0,
"createdAt": "2020-12-23T16:54:13Z"
},
"kinetic": {
"createdAt": "2020-12-25T15:16:48Z",
"moving": false
},
"privacy": {
"createdAt": "2020-12-25T15:16:43Z",
"state": "None"
},
"service": {
"type": "Electric",
"updatedAt": "2020-11-06T19:56:10Z"
},
"_links": {
"self": {
"href": "dummy_link/status"
},
"vehicles": {
"href": "dummy_link/vehicles"
}
},
"timed.odometer": {
"createdAt": null,
"mileage": 1539
},
"updatedAt": "2020-12-25T15:16:48Z"
}