[saicismart] Initial contribution (#15894)

* [saicismart] initial binding creation

Signed-off-by: Markus Heberling <markus@heberling.net>
Signed-off-by: dougculnane <doug@culnane.net>
This commit is contained in:
Doug Culnane 2024-04-27 22:49:34 +02:00 committed by GitHub
parent 229c2b7032
commit 652845fee5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1800 additions and 0 deletions

View File

@ -306,6 +306,7 @@
/bundles/org.openhab.binding.rotel/ @lolodomo
/bundles/org.openhab.binding.russound/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.sagercaster/ @clinique
/bundles/org.openhab.binding.saicismart/ @tisoft @dougculnane
/bundles/org.openhab.binding.samsungtv/ @paulianttila
/bundles/org.openhab.binding.satel/ @druciak
/bundles/org.openhab.binding.semsportal/ @itb3

View File

@ -1516,6 +1516,11 @@
<artifactId>org.openhab.binding.sagercaster</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.saicismart</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.samsungtv</artifactId>

View File

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

View File

@ -0,0 +1,131 @@
# SAICiSMART Binding
OpenHAB binding to the SAIC-API used by MG cars (MG4, MG5 EV, MG ZSV...)
It enables iSMART users to get battery status and other data from their cars.
They can also pre-heat their cars by turning ON the AC.
Based on the work done here: https://github.com/SAIC-iSmart-API
## Supported Things
European iSMART accounts and vehicles.
- `account`: Bridge representing an iSMART Account
- `vehicle`: Thing representing an iSMART MG Car
## Discovery
Vehicle discovery is implemented.
Once an account has been configured it can be scanned for vehicles.
## Thing Configuration
### `account` iSMART Account Configuration
| Name | Type | Description | Default | Required | Advanced |
|----------|---------|-----------------------------|---------|----------|----------|
| username | text | iSMART username | N/A | yes | no |
| password | text | iSMART password | N/A | yes | no |
### `vehicle` An iSMART MG Car
| Name | Type | Description | Default | Required | Advanced |
|---------------|------|--------------------------------------|---------|----------|----------|
| vin | text | Vehicle identification number (VIN) | N/A | yes | no |
| abrpUserToken | text | User token for A Better Routeplanner | N/A | no | no |
## Channels
| Channel | Type | Read/Write | Description | Advanced |
|----------------------------|--------------------------|------------|-----------------------------------------------------|----------|
| odometer | Number:Length | R | Total distance driven | no |
| range-electric | Number:Length | R | Electric range | no |
| soc | Number | R | State of the battery in % | no |
| power | Number:Power | R | Power usage | no |
| charging | Switch | R | Charging | no |
| engine | Switch | R | Engine state | no |
| speed | Number:Speed | R | Vehicle speed | no |
| location | Location | R | The actual position of the vehicle | no |
| heading | Number:Angle | R | The compass heading of the car, (0-360 degrees) | no |
| auxiliary-battery-voltage | Number:ElectricPotential | R | Auxiliary battery voltage | no |
| tyre-pressure-front-left | Number:Pressure | R | Pressure front left | no |
| tyre-pressure-front-right | Number:Pressure | R | Pressure front right | no |
| tyre-pressure-rear-left | Number:Pressure | R | Pressure rear left | no |
| tyre-pressure-rear-right | Number:Pressure | R | Pressure rear right | no |
| interior-temperature | Number:Temperature | R | Interior temperature | no |
| exterior-temperature | Number:Temperature | R | Exterior temperature | no |
| door-driver | Contact | R | Driver door open state | no |
| door-passenger | Contact | R | Passenger door open state | no |
| door-rear-left | Contact | R | Rear left door open state | no |
| door-rear-right | Contact | R | Rear right door open state | no |
| window-driver | Contact | R | Driver window open state | no |
| window-passenger | Contact | R | Passenger window open state | no |
| window-rear-left | Contact | R | Rear left window open state | no |
| window-rear-right | Contact | R | Rear right window open state | no |
| window-sun-roof | Contact | R | Sun roof open state | no |
| last-activity | DateTime | R | Last time the engine was on or the car was charging | no |
| last-position-update | DateTime | R | Last time the Position data was updated | no |
| last-charge-state-update | DateTime | R | Last time the Charge State data was updated | no |
| remote-ac-status | Number | R | Status of the A/C | no |
| switch-ac | Switch | R/W | Control the A/C remotely | no |
| force-refresh | Switch | R/W | Force an immediate refresh of the car data | yes |
| last-alarm-message-date | DateTime | R | Last time an alarm message was sent | no |
| last-alarm-message-content | String | R | Vehicle message | no |
# Example
demo.things:
```java
Bridge saicismart:account:myaccount "My iSMART Account" [ username="MyEmail@domian.com", password="MyPassword" ] {
Thing vehicle mymg5 "MG5" [ vin="XXXXXXXXXXXXXXXXX", abrpUserToken="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ]
}
```
demo.items:
```java
Number MG5_Total_Distance_Driven "MG5 Total Distance Driven" ["Length"] {channel="saicismart:vehicle:myaccount:mymg5:odometer"}
Number MG5_Electric_Range "MG5 Electric Range" ["Length"] {channel="saicismart:vehicle:myaccount:mymg5:range-electric"}
Number MG5_Battery_Level "MG5 Battery Level" ["Energy"] {channel="saicismart:vehicle:myaccount:mymg5:soc"}
Number MG5_Power_Usage "MG5 Power Usage" ["Power"] {channel="saicismart:vehicle:myaccount:mymg5:power"}
Switch MG5_Charging "MG5 Charging" {channel="saicismart:vehicle:myaccount:mymg5:charging"}
Switch MG5_Engine_State "MG5 Engine State" {channel="saicismart:vehicle:myaccount:mymg5:engine"}
Number MG5_Speed "MG5 Speed" ["Speed"] {channel="saicismart:vehicle:myaccount:mymg5:speed"}
Location MG5_Location "MG5 Location" {channel="saicismart:vehicle:myaccount:mymg5:location"}
Number MG5_Heading "MG5 Heading" ["Angle"] {channel="saicismart:vehicle:myaccount:mymg5:heading"}
Number MG5_Auxiliary_Battery_Voltage "MG5 Auxiliary Battery Voltage" ["ElectricPotential"] {channel="saicismart:vehicle:myaccount:mymg5:auxiliary-battery-voltage"}
Number MG5_Pressure_Front_Left "MG5 Pressure Front Left" ["Pressure"] {channel="saicismart:vehicle:myaccount:mymg5:tyre-pressure-front-left"}
Number MG5_Pressure_Front_Right "MG5 Pressure Front Right ["Pressure"] {channel="saicismart:vehicle:myaccount:mymg5:tyre-pressure-front-right"}
Number MG5_Pressure_Rear_Left "MG5 Pressure Rear Left" ["Pressure"] {channel="saicismart:vehicle:myaccount:mymg5:tyre-pressure-rear-left"}
Number MG5_Pressure_Rear_Right "MG5 Pressure Rear Right" ["Pressure"] {channel="saicismart:vehicle:myaccount:mymg5:tyre-pressure-rear-right"}
Number MG5_Interior_Temperature "MG5 Interior Temperature" ["Temperature"] {channel="saicismart:vehicle:myaccount:mymg5:interior-temperature"}
Number MG5_Exterior_Temperature "MG5 Exterior Temperature" ["Temperature"] {channel="saicismart:vehicle:myaccount:mymg5:exterior-temperature"}
Contact MG5_Driver_Door "MG5 Driver Door" {channel="saicismart:vehicle:myaccount:mymg5:door-driver"}
Contact MG5_Passenger_Door "MG5 Passenger Door" {channel="saicismart:vehicle:myaccount:mymg5:door-passenger"}
Contact MG5_Rear_Left_Door "MG5 Rear Left Door" {channel="saicismart:vehicle:myaccount:mymg5:door-rear-left"}
Contact MG5_Rear_Right_Door "MG5 Rear Right Door" {channel="saicismart:vehicle:myaccount:mymg5:door-rear-right"}
Contact MG5_Driver_Window "MG5 Driver Window" {channel="saicismart:vehicle:myaccount:mymg5:window-driver"}
Contact MG5_Passenger_Window "MG5 Passenger Window" {channel="saicismart:vehicle:myaccount:mymg5:window-passenger"}
Contact MG5_Rear_Left_Window "MG5 Rear Left Window" {channel="saicismart:vehicle:myaccount:mymg5:window-rear-left"}
Contact MG5_Rear_Right_Window "MG5 Rear Right Window" {channel="saicismart:vehicle:myaccount:mymg5:window-rear-right"}
Contact MG5_Sun_Roof "MG5 Sun Roof" {channel="saicismart:vehicle:myaccount:mymg5:window-sun-roof"}
DateTime MG5_Last_Car_Activity "MG5 Last Car Activity" {channel="saicismart:vehicle:myaccount:mymg5:last-activity"}
DateTime MG5_Last_Position_Timestamp "MG5 Last Position Timestamp" {channel="saicismart:vehicle:myaccount:mymg5:last-position-update"}
DateTime MG5_Last_Charge_State_Timestamp "MG5 Last Charge State Timestamp" {channel="saicismart:vehicle:myaccount:mymg5:last-charge-state-update"}
Number MG5_Remote_AC "MG5 Remote A/C" {channel="saicismart:vehicle:myaccount:mymg5:remote-ac-status"}
Switch MG5_Switch_AC "MG5 Switch A/C" {channel="saicismart:vehicle:myaccount:mymg5:switch-ac"}
Switch MG5_Force_Refresh "MG5 Force Refresh" {channel="saicismart:vehicle:myaccount:mymg5:force-refresh"}
DateTime MG5_Last_Alarm_Message_Timestamp "MG5 Last Alarm Message Timestamp" {channel="saicismart:vehicle:myaccount:mymg5:last-alarm-message-date"}
String MG5_Vehicle_Message "MG5 Vehicle Message" {channel="saicismart:vehicle:myaccount:mymg5:last-alarm-message-content"}
```
## Limitations
The advanced channel "force refresh" if used regularly will drain the 12v car battery and you will be unable to start it!
Only European iSMART accounts and vehicles are supported. API host configuration and testing for other markets is required.

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.2.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.saicismart</artifactId>
<name>openHAB Add-ons :: Bundles :: SAICiSMART Binding</name>
<properties>
<bnd.importpackage>org.brotli.dec;resolution:=optional,org.conscrypt;resolution:=optional</bnd.importpackage>
</properties>
<dependencies>
<dependency>
<groupId>io.github.saic-ismart-api</groupId>
<artifactId>saic-ismart-client</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>io.github.saic-ismart-api</groupId>
<artifactId>saic-ismart-api</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>5.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
<version>5.2</version>
</dependency>
<dependency>
<groupId>net.heberling.binarynotes</groupId>
<artifactId>binarynotes</artifactId>
<version>1.7.0</version>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -0,0 +1,113 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.API_ENDPOINT_V30;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_POWER;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_SOC;
import java.net.URISyntaxException;
import java.time.ZonedDateTime;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.bn.coders.IASN1PreparedElement;
import org.eclipse.jdt.annotation.DefaultLocation;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.saicismart.internal.exceptions.ChargingStatusAPIException;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ThingStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.GsonBuilder;
import net.heberling.ismart.asn1.v3_0.Message;
import net.heberling.ismart.asn1.v3_0.MessageCoder;
import net.heberling.ismart.asn1.v3_0.entity.OTA_ChrgMangDataResp;
/**
*
* @author Markus Heberling - Initial contribution
*/
@NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.FIELD,
DefaultLocation.TYPE_BOUND })
class ChargeStateUpdater implements Callable<OTA_ChrgMangDataResp> {
private final Logger logger = LoggerFactory.getLogger(ChargeStateUpdater.class);
private final SAICiSMARTHandler saiCiSMARTHandler;
public ChargeStateUpdater(SAICiSMARTHandler saiCiSMARTHandler) {
this.saiCiSMARTHandler = saiCiSMARTHandler;
}
public OTA_ChrgMangDataResp call() throws URISyntaxException, ExecutionException, InterruptedException,
TimeoutException, ChargingStatusAPIException {
MessageCoder<IASN1PreparedElement> chargingStatusRequestmessageCoder = new MessageCoder<>(
IASN1PreparedElement.class);
Message<IASN1PreparedElement> chargingStatusMessage = chargingStatusRequestmessageCoder.initializeMessage(
saiCiSMARTHandler.getBridgeHandler().getUid(), saiCiSMARTHandler.getBridgeHandler().getToken(),
saiCiSMARTHandler.config.vin, "516", 768, 5, null);
String chargingStatusRequestMessage = chargingStatusRequestmessageCoder.encodeRequest(chargingStatusMessage);
String chargingStatusResponse = saiCiSMARTHandler.getBridgeHandler().sendRequest(chargingStatusRequestMessage,
API_ENDPOINT_V30);
Message<OTA_ChrgMangDataResp> chargingStatusResponseMessage = new MessageCoder<>(OTA_ChrgMangDataResp.class)
.decodeResponse(chargingStatusResponse);
// we get an eventId back...
chargingStatusMessage.getBody().setEventID(chargingStatusResponseMessage.getBody().getEventID());
// ... use that to request the data again, until we have it
while (chargingStatusResponseMessage.getApplicationData() == null) {
if (chargingStatusResponseMessage.getBody().getResult() != 0
|| chargingStatusResponseMessage.getBody().isErrorMessagePresent()) {
if (chargingStatusResponseMessage.getBody().getResult() == 2) {
saiCiSMARTHandler.getBridgeHandler().relogin();
}
throw new ChargingStatusAPIException(chargingStatusResponseMessage.getBody());
}
chargingStatusMessage.getBody().setUid(saiCiSMARTHandler.getBridgeHandler().getUid());
chargingStatusMessage.getBody().setToken(saiCiSMARTHandler.getBridgeHandler().getToken());
chargingStatusRequestMessage = chargingStatusRequestmessageCoder.encodeRequest(chargingStatusMessage);
chargingStatusResponse = saiCiSMARTHandler.getBridgeHandler().sendRequest(chargingStatusRequestMessage,
API_ENDPOINT_V30);
chargingStatusResponseMessage = new MessageCoder<>(OTA_ChrgMangDataResp.class)
.decodeResponse(chargingStatusResponse);
}
saiCiSMARTHandler.updateState(CHANNEL_SOC,
new DecimalType(chargingStatusResponseMessage.getApplicationData().getBmsPackSOCDsp() / 10.d));
logger.debug("Got message: {}",
new GsonBuilder().setPrettyPrinting().create().toJson(chargingStatusResponseMessage));
Double power = (chargingStatusResponseMessage.getApplicationData().getBmsPackCrnt() * 0.05d - 1000.0d)
* ((double) chargingStatusResponseMessage.getApplicationData().getBmsPackVol() * 0.25d);
saiCiSMARTHandler.updateState(CHANNEL_POWER, new QuantityType<>(power.intValue(), Units.WATT));
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_LAST_CHARGE_STATE_UPDATE,
new DateTimeType(ZonedDateTime.now(saiCiSMARTHandler.getTimeZone())));
saiCiSMARTHandler.updateStatus(ThingStatus.ONLINE);
return chargingStatusResponseMessage.getApplicationData();
}
}

View File

@ -0,0 +1,99 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SAICiSMARTBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Markus Heberling - Initial contribution
*/
@NonNullByDefault
public class SAICiSMARTBindingConstants {
private static final String BINDING_ID = "saicismart";
/**
* Interval in seconds between polls of API.
*/
public static final int REFRESH_INTERVAL = 10;
/**
* Active polling period in minutes
*/
public static final int POLLING_ACTIVE_MINS = 10;
/**
* URL of the SAIC API Host.
*/
private static final String API_HOST_URL = "https://tap-eu.soimt.com";
/**
* https://github.com/SAIC-iSmart-API/documentation?tab=readme-ov-file#api-v11
*/
public static final String API_ENDPOINT_V11 = API_HOST_URL + "/TAP.Web/ota.mp";
/**
* https://github.com/SAIC-iSmart-API/documentation?tab=readme-ov-file#api-v21
*/
public static final String API_ENDPOINT_V21 = API_HOST_URL + "/TAP.Web/ota.mpv21";
/**
* https://github.com/SAIC-iSmart-API/documentation?tab=readme-ov-file#api-v30
*/
public static final String API_ENDPOINT_V30 = API_HOST_URL + "/TAP.Web/ota.mpv30";
public static final String ABRP_API_KEY = "8cfc314b-03cd-4efe-ab7d-4431cd8f2e2d";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_VEHICLE = new ThingTypeUID(BINDING_ID, "vehicle");
// List of all Channel ids
public static final String CHANNEL_ODOMETER = "odometer";
public static final String CHANNEL_RANGE_ELECTRIC = "range-electric";
public static final String CHANNEL_SOC = "soc";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_ENGINE = "engine";
public static final String CHANNEL_CHARGING = "charging";
public static final String CHANNEL_TYRE_PRESSURE_FRONT_LEFT = "tyre-pressure-front-left";
public static final String CHANNEL_TYRE_PRESSURE_FRONT_RIGHT = "tyre-pressure-front-right";
public static final String CHANNEL_TYRE_PRESSURE_REAR_LEFT = "tyre-pressure-rear-left";
public static final String CHANNEL_TYRE_PRESSURE_REAR_RIGHT = "tyre-pressure-rear-right";
public static final String CHANNEL_INTERIOR_TEMPERATURE = "interior-temperature";
public static final String CHANNEL_EXTERIOR_TEMPERATURE = "exterior-temperature";
public static final String CHANNEL_SPEED = "speed";
public static final String CHANNEL_LOCATION = "location";
public static final String CHANNEL_HEADING = "heading";
public static final String CHANNEL_AUXILIARY_BATTERY_VOLTAGE = "auxiliary-battery-voltage";
public static final String CHANNEL_DOOR_DRIVER = "door-driver";
public static final String CHANNEL_DOOR_PASSENGER = "door-passenger";
public static final String CHANNEL_DOOR_REAR_LEFT = "door-rear-left";
public static final String CHANNEL_DOOR_REAR_RIGHT = "door-rear-right";
public static final String CHANNEL_WINDOW_DRIVER = "window-driver";
public static final String CHANNEL_WINDOW_PASSENGER = "window-passenger";
public static final String CHANNEL_WINDOW_REAR_LEFT = "window-rear-left";
public static final String CHANNEL_WINDOW_REAR_RIGHT = "window-rear-right";
public static final String CHANNEL_WINDOW_SUN_ROOF = "window-sun-roof";
public static final String CHANNEL_LAST_ACTIVITY = "last-activity";
public static final String CHANNEL_FORCE_REFRESH = "force-refresh";
public static final String CHANNEL_REMOTE_AC_STATUS = "remote-ac-status";
public static final String CHANNEL_SWITCH_AC = "switch-ac";
public static final String CHANNEL_LAST_POSITION_UPDATE = "last-position-update";
public static final String CHANNEL_LAST_CHARGE_STATE_UPDATE = "last-charge-state-update";
public static final String CHANNEL_ALARM_MESSAGE_DATE = "last-alarm-message-date";
public static final String CHANNEL_ALARM_MESSAGE_CONTENT = "last-alarm-message-content";
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SAICiSMARTBridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Markus Heberling - Initial contribution
*/
@NonNullByDefault
public class SAICiSMARTBridgeConfiguration {
public String username = "";
public String password = "";
}

View File

@ -0,0 +1,297 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.API_ENDPOINT_V11;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.bind.DatatypeConverter;
import org.bn.coders.IASN1PreparedElement;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.GsonBuilder;
import net.heberling.ismart.asn1.v1_1.Message;
import net.heberling.ismart.asn1.v1_1.MessageCoder;
import net.heberling.ismart.asn1.v1_1.entity.AlarmSwitch;
import net.heberling.ismart.asn1.v1_1.entity.AlarmSwitchReq;
import net.heberling.ismart.asn1.v1_1.entity.MP_AlarmSettingType;
import net.heberling.ismart.asn1.v1_1.entity.MP_UserLoggingInReq;
import net.heberling.ismart.asn1.v1_1.entity.MP_UserLoggingInResp;
import net.heberling.ismart.asn1.v1_1.entity.MessageListReq;
import net.heberling.ismart.asn1.v1_1.entity.MessageListResp;
import net.heberling.ismart.asn1.v1_1.entity.StartEndNumber;
import net.heberling.ismart.asn1.v1_1.entity.VinInfo;
/**
* The {@link SAICiSMARTBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Markus Heberling - Initial contribution
*/
@NonNullByDefault
public class SAICiSMARTBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(SAICiSMARTBridgeHandler.class);
private @Nullable SAICiSMARTBridgeConfiguration config;
private @Nullable String uid;
private @Nullable String token;
private @Nullable Collection<VinInfo> vinList;
private HttpClient httpClient;
private @Nullable Future<?> pollingJob;
public SAICiSMARTBridgeHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// no commands available
}
@Override
public void initialize() {
config = getConfigAs(SAICiSMARTBridgeConfiguration.class);
updateStatus(ThingStatus.UNKNOWN);
// Validate configuration
if (this.config.username.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/thing-type.config.saicismart.bridge.username.required");
return;
}
if (this.config.password.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/thing-type.config.saicismart.bridge.password.required");
return;
}
if (this.config.username.length() > 50) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/thing-type.config.saicismart.bridge.username.toolong");
return;
}
pollingJob = scheduler.scheduleWithFixedDelay(this::updateStatus, 1,
SAICiSMARTBindingConstants.REFRESH_INTERVAL, TimeUnit.SECONDS);
}
private void updateStatus() {
if (uid == null || token == null) {
login();
} else {
registerForMessages();
}
}
private void login() {
MessageCoder<MP_UserLoggingInReq> mpUserLoggingInRequestMessageCoder = new MessageCoder<>(
MP_UserLoggingInReq.class);
MP_UserLoggingInReq mpUserLoggingInReq = new MP_UserLoggingInReq();
mpUserLoggingInReq.setPassword(config.password);
Message<MP_UserLoggingInReq> loginRequestMessage = mpUserLoggingInRequestMessageCoder.initializeMessage(
StringUtils.padLeft("#" + config.username, 50, "0"), null, null, "501", 513, 1, mpUserLoggingInReq);
String loginRequest = mpUserLoggingInRequestMessageCoder.encodeRequest(loginRequestMessage);
try {
String loginResponse = sendRequest(loginRequest, API_ENDPOINT_V11);
Message<MP_UserLoggingInResp> loginResponseMessage = new MessageCoder<>(MP_UserLoggingInResp.class)
.decodeResponse(loginResponse);
logger.trace("Got message: {}",
new GsonBuilder().setPrettyPrinting().create().toJson(loginResponseMessage));
uid = loginResponseMessage.getBody().getUid();
token = loginResponseMessage.getApplicationData().getToken();
vinList = loginResponseMessage.getApplicationData().getVinList();
// register for all known alarm types (not all might be actually delivered)
for (MP_AlarmSettingType.EnumType type : MP_AlarmSettingType.EnumType.values()) {
registerAlarmMessage(loginResponseMessage.getBody().getUid(),
loginResponseMessage.getApplicationData().getToken(), type);
}
updateStatus(ThingStatus.ONLINE);
} catch (TimeoutException | URISyntaxException | ExecutionException | InterruptedException
| NoSuchAlgorithmException | IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private void registerForMessages() {
MessageCoder<MessageListReq> messageListReqMessageCoder = new MessageCoder<>(MessageListReq.class);
Message<MessageListReq> messageListRequestMessage = messageListReqMessageCoder.initializeMessage(uid, token,
null, "531", 513, 1, new MessageListReq());
messageListRequestMessage.getHeader().setProtocolVersion(18);
// We currently assume that the newest message is the first.
messageListRequestMessage.getApplicationData().setStartEndNumber(new StartEndNumber());
messageListRequestMessage.getApplicationData().getStartEndNumber().setStartNumber(1L);
messageListRequestMessage.getApplicationData().getStartEndNumber().setEndNumber(5L);
messageListRequestMessage.getApplicationData().setMessageGroup("ALARM");
String messageListRequest = messageListReqMessageCoder.encodeRequest(messageListRequestMessage);
try {
String messageListResponse = sendRequest(messageListRequest, API_ENDPOINT_V11);
Message<MessageListResp> messageListResponseMessage = new MessageCoder<>(MessageListResp.class)
.decodeResponse(messageListResponse);
logger.trace("Got message: {}",
new GsonBuilder().setPrettyPrinting().create().toJson(messageListResponseMessage));
if (messageListResponseMessage.getApplicationData() != null
&& messageListResponseMessage.getApplicationData().getMessages() != null) {
for (net.heberling.ismart.asn1.v1_1.entity.Message message : messageListResponseMessage
.getApplicationData().getMessages()) {
if (message.isVinPresent()) {
String vin = message.getVin();
getThing().getThings().stream().filter(t -> t.getUID().getId().equals(vin))
.map(Thing::getHandler).filter(Objects::nonNull)
.filter(SAICiSMARTHandler.class::isInstance).map(SAICiSMARTHandler.class::cast)
.forEach(t -> t.handleMessage(message));
}
}
}
updateStatus(ThingStatus.ONLINE);
} catch (TimeoutException | URISyntaxException | ExecutionException | InterruptedException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private void registerAlarmMessage(String uid, String token, MP_AlarmSettingType.EnumType type)
throws NoSuchAlgorithmException, IOException, URISyntaxException, ExecutionException, InterruptedException,
TimeoutException {
MessageCoder<AlarmSwitchReq> alarmSwitchReqMessageCoder = new MessageCoder<>(AlarmSwitchReq.class);
AlarmSwitchReq alarmSwitchReq = new AlarmSwitchReq();
alarmSwitchReq
.setAlarmSwitchList(Stream.of(type).map(v -> createAlarmSwitch(v, true)).collect(Collectors.toList()));
alarmSwitchReq.setPin(hashMD5("123456"));
Message<AlarmSwitchReq> alarmSwitchMessage = alarmSwitchReqMessageCoder.initializeMessage(uid, token, null,
"521", 513, 1, alarmSwitchReq);
String alarmSwitchRequest = alarmSwitchReqMessageCoder.encodeRequest(alarmSwitchMessage);
String alarmSwitchResponse = sendRequest(alarmSwitchRequest, API_ENDPOINT_V11);
final MessageCoder<IASN1PreparedElement> alarmSwitchResMessageCoder = new MessageCoder<>(
IASN1PreparedElement.class);
Message<IASN1PreparedElement> alarmSwitchResponseMessage = alarmSwitchResMessageCoder
.decodeResponse(alarmSwitchResponse);
logger.trace("Got message: {}",
new GsonBuilder().setPrettyPrinting().create().toJson(alarmSwitchResponseMessage));
if (alarmSwitchResponseMessage.getBody().getErrorMessage() != null) {
logger.debug("Could not register for {} messages: {}", type,
new String(alarmSwitchResponseMessage.getBody().getErrorMessage(), StandardCharsets.UTF_8));
} else {
logger.debug("Registered for {} messages", type);
}
}
private static AlarmSwitch createAlarmSwitch(MP_AlarmSettingType.EnumType type, boolean enabled) {
AlarmSwitch alarmSwitch = new AlarmSwitch();
MP_AlarmSettingType alarmSettingType = new MP_AlarmSettingType();
alarmSettingType.setValue(type);
alarmSettingType.setIntegerForm(type.ordinal());
alarmSwitch.setAlarmSettingType(alarmSettingType);
alarmSwitch.setAlarmSwitch(enabled);
alarmSwitch.setFunctionSwitch(enabled);
return alarmSwitch;
}
public String hashMD5(String password) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(password.getBytes());
byte[] digest = md.digest();
return DatatypeConverter.printHexBinary(digest).toUpperCase();
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(VehicleDiscovery.class);
}
@Nullable
public String getUid() {
return uid;
}
@Nullable
public String getToken() {
return token;
}
public Collection<VinInfo> getVinList() {
return Optional.ofNullable(vinList).orElse(Collections.emptyList());
}
public String sendRequest(String request, String endpoint)
throws URISyntaxException, ExecutionException, InterruptedException, TimeoutException {
return httpClient.POST(new URI(endpoint)).content(new StringContentProvider(request), "text/html").send()
.getContentAsString();
}
public void relogin() {
uid = null;
token = null;
}
@Override
public void dispose() {
Future<?> job = pollingJob;
if (job != null) {
job.cancel(true);
pollingJob = null;
}
}
}

View File

@ -0,0 +1,283 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.ABRP_API_KEY;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.API_ENDPOINT_V21;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_FORCE_REFRESH;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_LAST_ACTIVITY;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_SWITCH_AC;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
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.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.GsonBuilder;
import net.heberling.ismart.abrp.ABRP;
import net.heberling.ismart.asn1.v1_1.entity.Message;
import net.heberling.ismart.asn1.v2_1.MessageCoder;
import net.heberling.ismart.asn1.v2_1.entity.OTA_RVCReq;
import net.heberling.ismart.asn1.v2_1.entity.OTA_RVCStatus25857;
import net.heberling.ismart.asn1.v2_1.entity.OTA_RVMVehicleStatusResp25857;
import net.heberling.ismart.asn1.v2_1.entity.RvcReqParam;
import net.heberling.ismart.asn1.v3_0.entity.OTA_ChrgMangDataResp;
/**
* The {@link SAICiSMARTHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Markus Heberling - Initial contribution
*/
@NonNullByDefault
public class SAICiSMARTHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(SAICiSMARTHandler.class);
private final TimeZoneProvider timeZoneProvider;
@Nullable
SAICiSMARTVehicleConfiguration config;
private @Nullable Future<?> pollingJob;
private ZonedDateTime lastAlarmMessage;
private ZonedDateTime lastCarActivity;
/**
* If the binding is initialized, treat the car as active (lastCarActivity = now) to get some first data.
*
* @param httpClientFactory
* @param timeZoneProvider
* @param thing
*/
public SAICiSMARTHandler(TimeZoneProvider timeZoneProvider, Thing thing) {
super(thing);
this.timeZoneProvider = timeZoneProvider;
lastAlarmMessage = ZonedDateTime.now(getTimeZone());
lastCarActivity = ZonedDateTime.now(getTimeZone());
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (channelUID.getId().equals(SAICiSMARTBindingConstants.CHANNEL_FORCE_REFRESH) && command == OnOffType.ON) {
// reset channel to off
updateState(CHANNEL_FORCE_REFRESH, OnOffType.from(false));
// update internal activity date, to query the car for about a minute
notifyCarActivity(ZonedDateTime.now(getTimeZone()).minus(SAICiSMARTBindingConstants.POLLING_ACTIVE_MINS - 1,
ChronoUnit.MINUTES), true);
} else if (channelUID.getId().equals(CHANNEL_SWITCH_AC) && command == OnOffType.ON) {
// reset channel to off
updateState(CHANNEL_SWITCH_AC, OnOffType.ON);
// enable air conditioning
try {
sendACCommand((byte) 5, (byte) 8);
} catch (URISyntaxException | ExecutionException | TimeoutException | InterruptedException e) {
logger.warn("A/C On Command failed", e);
}
} else if (channelUID.getId().equals(CHANNEL_SWITCH_AC) && command == OnOffType.OFF) {
// reset channel to off
updateState(CHANNEL_SWITCH_AC, OnOffType.OFF);
// disable air conditioning
try {
sendACCommand((byte) 0, (byte) 0);
} catch (URISyntaxException | ExecutionException | TimeoutException | InterruptedException e) {
logger.warn("A/C Off Command failed", e);
}
} else if (channelUID.getId().equals(CHANNEL_LAST_ACTIVITY)
&& command instanceof DateTimeType commnadAsDateTimeType) {
// update internal activity date from external date
notifyCarActivity(commnadAsDateTimeType.getZonedDateTime(), true);
}
}
protected @Nullable SAICiSMARTBridgeHandler getBridgeHandler() {
return (SAICiSMARTBridgeHandler) super.getBridge().getHandler();
}
@Override
public void initialize() {
config = getConfigAs(SAICiSMARTVehicleConfiguration.class);
ThingBuilder thingBuilder = editThing();
updateThing(thingBuilder.build());
updateStatus(ThingStatus.UNKNOWN);
// Validate configuration
if (this.config.vin.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/thing-type.config.saicismart.vehicle.vin.required");
return;
}
// just started, make sure we start querying
notifyCarActivity(ZonedDateTime.now(getTimeZone()), true);
pollingJob = scheduler.scheduleWithFixedDelay(this::updateStatus, 2,
SAICiSMARTBindingConstants.REFRESH_INTERVAL, TimeUnit.SECONDS);
}
private void updateStatus() {
if (lastCarActivity.isAfter(
ZonedDateTime.now().minus(SAICiSMARTBindingConstants.POLLING_ACTIVE_MINS, ChronoUnit.MINUTES))) {
if (this.getBridgeHandler().getUid() != null && this.getBridgeHandler().getToken() != null) {
try {
OTA_RVMVehicleStatusResp25857 otaRvmVehicleStatusResp25857 = new VehicleStateUpdater(this).call();
OTA_ChrgMangDataResp otaChrgMangDataResp = new ChargeStateUpdater(this).call();
if (config.abrpUserToken != null && config.abrpUserToken.length() > 0) {
String execute = ABRP.updateAbrp(ABRP_API_KEY, config.abrpUserToken,
otaRvmVehicleStatusResp25857, otaChrgMangDataResp);
logger.debug("ABRP: {}", execute);
}
} catch (Exception e) {
logger.warn("Could not refresh car data.", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/addon.saicismart.error.refresh.car.data");
}
}
}
}
private void sendACCommand(byte command, byte temperature)
throws URISyntaxException, ExecutionException, InterruptedException, TimeoutException {
MessageCoder<OTA_RVCReq> otaRvcReqMessageCoder = new MessageCoder<>(OTA_RVCReq.class);
// we send a command end expect the car to wake up
notifyCarActivity(ZonedDateTime.now(getTimeZone()), false);
OTA_RVCReq req = new OTA_RVCReq();
req.setRvcReqType(new byte[] { 6 });
List<RvcReqParam> params = new ArrayList<>();
req.setRvcParams(params);
RvcReqParam param = new RvcReqParam();
param.setParamId(19);
param.setParamValue(new byte[] { command });
params.add(param);
param = new RvcReqParam();
param.setParamId(20);
param.setParamValue(new byte[] { temperature });
params.add(param);
param = new RvcReqParam();
param.setParamId(255);
param.setParamValue(new byte[] { 0 });
params.add(param);
net.heberling.ismart.asn1.v2_1.Message<OTA_RVCReq> enableACRequest = otaRvcReqMessageCoder.initializeMessage(
getBridgeHandler().getUid(), getBridgeHandler().getToken(), config.vin, "510", 25857, 1, req);
String enableACRequestMessage = otaRvcReqMessageCoder.encodeRequest(enableACRequest);
String enableACResponseMessage = getBridgeHandler().sendRequest(enableACRequestMessage, API_ENDPOINT_V21);
net.heberling.ismart.asn1.v2_1.Message<OTA_RVCStatus25857> enableACResponse = new net.heberling.ismart.asn1.v2_1.MessageCoder<>(
OTA_RVCStatus25857.class).decodeResponse(enableACResponseMessage);
// ... use that to request the data again, until we have it
while (enableACResponse.getApplicationData() == null) {
if (enableACResponse.getBody().isErrorMessagePresent()) {
if (enableACResponse.getBody().getResult() == 2) {
getBridgeHandler().relogin();
}
throw new TimeoutException(new String(enableACResponse.getBody().getErrorMessage()));
}
if (enableACResponse.getBody().getResult() == 0) {
// we get an eventId back...
enableACRequest.getBody().setEventID(enableACResponse.getBody().getEventID());
} else {
// try a fresh eventId
enableACRequest.getBody().setEventID(0);
}
enableACRequestMessage = otaRvcReqMessageCoder.encodeRequest(enableACRequest);
enableACResponseMessage = getBridgeHandler().sendRequest(enableACRequestMessage, API_ENDPOINT_V21);
enableACResponse = new net.heberling.ismart.asn1.v2_1.MessageCoder<>(OTA_RVCStatus25857.class)
.decodeResponse(enableACResponseMessage);
}
logger.trace("Got A/C message: {}", new GsonBuilder().setPrettyPrinting().create().toJson(enableACResponse));
}
public void notifyCarActivity(ZonedDateTime now, boolean force) {
// if the car activity changed, notify the channel
if (force || lastCarActivity.isBefore(now)) {
lastCarActivity = now;
updateState(CHANNEL_LAST_ACTIVITY, new DateTimeType(lastCarActivity));
}
}
@Override
public void dispose() {
Future<?> job = pollingJob;
if (job != null) {
job.cancel(true);
pollingJob = null;
}
}
@Override
public void updateState(String channelID, State state) {
super.updateState(channelID, state);
}
@Override
public void updateStatus(ThingStatus status) {
super.updateStatus(status);
}
public void handleMessage(Message message) {
ZonedDateTime time = ZonedDateTime.ofInstant(Instant.ofEpochSecond(message.getMessageTime().getSeconds()),
getTimeZone());
if (time.isAfter(lastAlarmMessage)) {
lastAlarmMessage = time;
updateState(SAICiSMARTBindingConstants.CHANNEL_ALARM_MESSAGE_CONTENT,
new StringType(new String(message.getContent(), StandardCharsets.UTF_8)));
updateState(SAICiSMARTBindingConstants.CHANNEL_ALARM_MESSAGE_DATE, new DateTimeType(time));
}
notifyCarActivity(time, false);
}
public ZoneId getTimeZone() {
return timeZoneProvider.getTimeZone();
}
}

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.*;
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.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.TranslationProvider;
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 SAICiSMARTHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Markus Heberling - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.saicismart", service = ThingHandlerFactory.class)
public class SAICiSMARTHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_VEHICLE);
private final TimeZoneProvider timeZoneProvider;
private HttpClient httpClient;
@Activate
public SAICiSMARTHandlerFactory(final @Reference TranslationProvider translationProvider,
final @Reference LocaleProvider localeProvider, final @Reference HttpClientFactory httpClientFactory,
final @Reference TimeZoneProvider timeZoneProvider) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.timeZoneProvider = timeZoneProvider;
}
@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_ACCOUNT.equals(thingTypeUID)) {
return new SAICiSMARTBridgeHandler((Bridge) thing, httpClient);
} else if (THING_TYPE_VEHICLE.equals(thingTypeUID)) {
return new SAICiSMARTHandler(timeZoneProvider, thing);
}
return null;
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link SAICiSMARTVehicleConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Markus Heberling - Initial contribution
*/
@NonNullByDefault
public class SAICiSMARTVehicleConfiguration {
public String vin = "";
@Nullable
public String abrpUserToken;
}

View File

@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.THING_TYPE_VEHICLE;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.config.discovery.DiscoveryService;
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 net.heberling.ismart.asn1.v1_1.entity.VinInfo;
/**
*
* @author Markus Heberling - Initial contribution
*/
@NonNullByDefault
public class VehicleDiscovery extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
private @Nullable SAICiSMARTBridgeHandler handler;
private static final String PROPERTY_VIN = "vin";
public VehicleDiscovery() throws IllegalArgumentException {
super(Set.of(THING_TYPE_VEHICLE), 0);
}
@Override
protected void startScan() {
Collection<VinInfo> vinList = handler.getVinList();
for (VinInfo vinInfo : vinList) {
ThingTypeUID type = THING_TYPE_VEHICLE;
ThingUID thingUID = new ThingUID(type, handler.getThing().getUID(), vinInfo.getVin());
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
.withLabel(new String(vinInfo.getBrandName()) + " " + new String(vinInfo.getModelName()))
.withBridge(handler.getThing().getUID()).withProperty(PROPERTY_VIN, vinInfo.getVin())
.withRepresentationProperty(PROPERTY_VIN).build();
thingDiscovered(discoveryResult);
}
}
@Override
public void setThingHandler(ThingHandler handler) {
this.handler = (SAICiSMARTBridgeHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@Override
public void activate(@Nullable Map<String, Object> configProperties) {
super.activate(configProperties);
}
@Override
public void deactivate() {
super.deactivate();
}
}

View File

@ -0,0 +1,242 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.API_ENDPOINT_V21;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_AUXILIARY_BATTERY_VOLTAGE;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_CHARGING;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_ENGINE;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_HEADING;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_LOCATION;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_ODOMETER;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_RANGE_ELECTRIC;
import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.CHANNEL_SPEED;
import java.net.URISyntaxException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.saicismart.internal.exceptions.VehicleStatusAPIException;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
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.ThingStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.GsonBuilder;
import net.heberling.ismart.asn1.v2_1.Message;
import net.heberling.ismart.asn1.v2_1.MessageCoder;
import net.heberling.ismart.asn1.v2_1.entity.OTA_RVMVehicleStatusReq;
import net.heberling.ismart.asn1.v2_1.entity.OTA_RVMVehicleStatusResp25857;
/**
* @author Markus Heberling - Initial contribution
*/
@NonNullByDefault
class VehicleStateUpdater implements Callable<OTA_RVMVehicleStatusResp25857> {
private final Logger logger = LoggerFactory.getLogger(VehicleStateUpdater.class);
private final SAICiSMARTHandler saiCiSMARTHandler;
public VehicleStateUpdater(SAICiSMARTHandler saiCiSMARTHandler) {
this.saiCiSMARTHandler = saiCiSMARTHandler;
}
@Override
public OTA_RVMVehicleStatusResp25857 call() throws URISyntaxException, ExecutionException, InterruptedException,
TimeoutException, VehicleStatusAPIException {
MessageCoder<OTA_RVMVehicleStatusReq> otaRvmVehicleStatusRequstMessageCoder = new MessageCoder<>(
OTA_RVMVehicleStatusReq.class);
OTA_RVMVehicleStatusReq otaRvmVehicleStatusReq = new OTA_RVMVehicleStatusReq();
otaRvmVehicleStatusReq.setVehStatusReqType(1);
Message<OTA_RVMVehicleStatusReq> chargingStatusMessage = otaRvmVehicleStatusRequstMessageCoder
.initializeMessage(saiCiSMARTHandler.getBridgeHandler().getUid(),
saiCiSMARTHandler.getBridgeHandler().getToken(), saiCiSMARTHandler.config.vin, "511", 25857, 1,
otaRvmVehicleStatusReq);
String chargingStatusRequestMessage = otaRvmVehicleStatusRequstMessageCoder
.encodeRequest(chargingStatusMessage);
String chargingStatusResponse = saiCiSMARTHandler.getBridgeHandler().sendRequest(chargingStatusRequestMessage,
API_ENDPOINT_V21);
Message<OTA_RVMVehicleStatusResp25857> chargingStatusResponseMessage = new MessageCoder<>(
OTA_RVMVehicleStatusResp25857.class).decodeResponse(chargingStatusResponse);
// we get an eventId back...
chargingStatusMessage.getBody().setEventID(chargingStatusResponseMessage.getBody().getEventID());
// ... use that to request the data again, until we have it
while (chargingStatusResponseMessage.getApplicationData() == null) {
if (chargingStatusResponseMessage.getBody().getResult() != 0
|| chargingStatusResponseMessage.getBody().isErrorMessagePresent()) {
if (chargingStatusResponseMessage.getBody().getResult() == 2) {
saiCiSMARTHandler.getBridgeHandler().relogin();
}
throw new VehicleStatusAPIException(chargingStatusResponseMessage.getBody());
}
chargingStatusMessage.getBody().setUid(saiCiSMARTHandler.getBridgeHandler().getUid());
chargingStatusMessage.getBody().setToken(saiCiSMARTHandler.getBridgeHandler().getToken());
chargingStatusRequestMessage = otaRvmVehicleStatusRequstMessageCoder.encodeRequest(chargingStatusMessage);
chargingStatusResponse = saiCiSMARTHandler.getBridgeHandler().sendRequest(chargingStatusRequestMessage,
API_ENDPOINT_V21);
chargingStatusResponseMessage = new MessageCoder<>(OTA_RVMVehicleStatusResp25857.class)
.decodeResponse(chargingStatusResponse);
}
logger.trace("Got message: {}",
new GsonBuilder().setPrettyPrinting().create().toJson(chargingStatusResponseMessage));
boolean engineRunning = chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus()
.getEngineStatus() == 1;
boolean isCharging = chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus()
.isExtendedData2Present()
&& chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getExtendedData2() >= 1;
saiCiSMARTHandler.updateState(CHANNEL_ENGINE, OnOffType.from(engineRunning));
saiCiSMARTHandler.updateState(CHANNEL_CHARGING, OnOffType.from(isCharging));
saiCiSMARTHandler.updateState(CHANNEL_AUXILIARY_BATTERY_VOLTAGE, new QuantityType<>(
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getBatteryVoltage() / 10.d,
Units.VOLT));
saiCiSMARTHandler.updateState(CHANNEL_SPEED, new QuantityType<>(
chargingStatusResponseMessage.getApplicationData().getGpsPosition().getWayPoint().getSpeed() / 10.d,
SIUnits.KILOMETRE_PER_HOUR));
saiCiSMARTHandler.updateState(CHANNEL_HEADING,
new QuantityType<>(
chargingStatusResponseMessage.getApplicationData().getGpsPosition().getWayPoint().getHeading(),
Units.DEGREE_ANGLE));
saiCiSMARTHandler.updateState(CHANNEL_LOCATION,
new PointType(
new DecimalType(chargingStatusResponseMessage.getApplicationData().getGpsPosition()
.getWayPoint().getPosition().getLatitude() / 1000000d),
new DecimalType(chargingStatusResponseMessage.getApplicationData().getGpsPosition()
.getWayPoint().getPosition().getLongitude() / 1000000d),
new DecimalType(chargingStatusResponseMessage.getApplicationData().getGpsPosition()
.getWayPoint().getPosition().getAltitude())));
saiCiSMARTHandler.updateState(CHANNEL_ODOMETER,
new QuantityType<>(
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getMileage() / 10.d,
MetricPrefix.KILO(SIUnits.METRE)));
saiCiSMARTHandler.updateState(CHANNEL_RANGE_ELECTRIC, new QuantityType<>(
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getFuelRangeElec() / 10.d,
MetricPrefix.KILO(SIUnits.METRE)));
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_TYRE_PRESSURE_FRONT_LEFT, new QuantityType<>(
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getFrontLeftTyrePressure()
* 4 / 100.d,
Units.BAR));
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_TYRE_PRESSURE_FRONT_RIGHT, new QuantityType<>(
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getFrontRrightTyrePressure()
* 4 / 100.d,
Units.BAR));
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_TYRE_PRESSURE_REAR_LEFT, new QuantityType<>(
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getRearLeftTyrePressure() * 4
/ 100.d,
Units.BAR));
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_TYRE_PRESSURE_REAR_RIGHT, new QuantityType<>(
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getRearRightTyrePressure()
* 4 / 100.d,
Units.BAR));
Integer interiorTemperature = chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus()
.getInteriorTemperature();
if (interiorTemperature > -128) {
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_INTERIOR_TEMPERATURE,
new QuantityType<>(interiorTemperature, SIUnits.CELSIUS));
}
Integer exteriorTemperature = chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus()
.getExteriorTemperature();
if (exteriorTemperature > -128) {
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_EXTERIOR_TEMPERATURE,
new QuantityType<>(exteriorTemperature, SIUnits.CELSIUS));
}
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_DOOR_DRIVER,
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getDriverDoor()
? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_DOOR_PASSENGER,
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getPassengerDoor()
? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_DOOR_REAR_LEFT,
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getRearLeftDoor()
? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_DOOR_REAR_RIGHT,
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getRearRightDoor()
? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_WINDOW_DRIVER,
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getDriverWindow()
? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_WINDOW_PASSENGER,
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getPassengerWindow()
? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_WINDOW_REAR_LEFT,
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getRearLeftWindow()
? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_WINDOW_REAR_RIGHT,
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getRearRightWindow()
? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_WINDOW_SUN_ROOF,
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getSunroofStatus()
? OpenClosedType.OPEN
: OpenClosedType.CLOSED);
boolean acActive = chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus()
.getRemoteClimateStatus() > 0;
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_SWITCH_AC, OnOffType.from(acActive));
saiCiSMARTHandler.updateState(SAICiSMARTBindingConstants.CHANNEL_REMOTE_AC_STATUS, new DecimalType(
chargingStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getRemoteClimateStatus()));
saiCiSMARTHandler
.updateState(SAICiSMARTBindingConstants.CHANNEL_LAST_POSITION_UPDATE,
new DateTimeType(ZonedDateTime.ofInstant(
Instant.ofEpochSecond(chargingStatusResponseMessage.getApplicationData()
.getGpsPosition().getTimestamp4Short().getSeconds()),
saiCiSMARTHandler.getTimeZone())));
if (isCharging || acActive || engineRunning) {
// update activity date
saiCiSMARTHandler.notifyCarActivity(ZonedDateTime.now(saiCiSMARTHandler.getTimeZone()), true);
}
saiCiSMARTHandler.updateStatus(ThingStatus.ONLINE);
return chargingStatusResponseMessage.getApplicationData();
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
import net.heberling.ismart.asn1.v3_0.MP_DispatcherBody;
/**
* @author Doug Culnane - Initial contribution
*/
@NonNullByDefault
public class ChargingStatusAPIException extends Exception {
public ChargingStatusAPIException(MP_DispatcherBody body) {
super("[" + body.getResult() + "] " + new String(body.getErrorMessage()));
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.saicismart.internal.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
import net.heberling.ismart.asn1.v2_1.MP_DispatcherBody;
/**
* @author Doug Culnane - Initial contribution
*/
@NonNullByDefault
public class VehicleStatusAPIException extends Exception {
public VehicleStatusAPIException(MP_DispatcherBody body) {
super("[" + body.getResult() + "] " + new String(body.getErrorMessage()));
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="saicismart" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>SAICiSMART Binding</name>
<description>This is the binding for SAIC (MG) iSMART Cars.</description>
<connection>cloud</connection>
</addon:addon>

View File

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

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:saicismart:vehicle">
<parameter name="vin" type="text" required="true">
<label>VIN</label>
<description>Unique Vehicle Identification Number (VIN) given by SAIC</description>
</parameter>
<parameter name="abrpUserToken" type="text" required="false">
<label>ABRP User Token</label>
<description>User Token for A Better Routeplanner.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="saicismart"
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="account">
<label>iSMART Account</label>
<description>Your iSMART account data</description>
<config-description-ref uri="thing-type:saicismart:bridge"/>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,229 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="saicismart"
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">
<!-- Sample Thing Type -->
<thing-type id="vehicle">
<supported-bridge-type-refs>
<bridge-type-ref id="account"/>
</supported-bridge-type-refs>
<label>SAIC Car</label>
<description>iSMART enabled car</description>
<category>Car</category>
<channels>
<channel id="odometer" typeId="odometer-channel"/>
<channel id="range-electric" typeId="range-electric-channel"/>
<channel id="soc" typeId="system.battery-level"/>
<channel id="power" typeId="power-channel"/>
<channel id="charging" typeId="charging-channel"/>
<channel id="engine" typeId="engine-channel"/>
<channel id="speed" typeId="speed-channel"/>
<channel id="location" typeId="location-channel"/>
<channel id="heading" typeId="heading-channel"/>
<channel id="auxiliary-battery-voltage" typeId="auxiliary-battery-voltage-channel"/>
<channel id="tyre-pressure-front-left" typeId="tyre-pressure-channel">
<label>Pressure Front Left</label>
</channel>
<channel id="tyre-pressure-front-right" typeId="tyre-pressure-channel">
<label>Pressure Front Right</label>
</channel>
<channel id="tyre-pressure-rear-left" typeId="tyre-pressure-channel">
<label>Pressure Rear Left</label>
</channel>
<channel id="tyre-pressure-rear-right" typeId="tyre-pressure-channel">
<label>Pressure Rear Right</label>
</channel>
<channel id="interior-temperature" typeId="temperature-channel">
<label>Interior Temperature</label>
</channel>
<channel id="exterior-temperature" typeId="temperature-channel">
<label>Exterior Temperature</label>
</channel>
<channel id="door-driver" typeId="door-channel">
<label>Driver Door</label>
</channel>
<channel id="door-passenger" typeId="door-channel">
<label>Passenger Door</label>
</channel>
<channel id="door-rear-left" typeId="door-channel">
<label>Rear Left Door</label>
</channel>
<channel id="door-rear-right" typeId="door-channel">
<label>Rear Right Door</label>
</channel>
<channel id="window-driver" typeId="window-channel">
<label>Driver Window</label>
</channel>
<channel id="window-passenger" typeId="window-channel">
<label>Passenger Window</label>
</channel>
<channel id="window-rear-left" typeId="window-channel">
<label>Rear Left Window</label>
</channel>
<channel id="window-rear-right" typeId="window-channel">
<label>Rear Right Window</label>
</channel>
<channel id="window-sun-roof" typeId="window-channel">
<label>Sun Roof</label>
</channel>
<channel id="last-activity" typeId="timestamp-channel">
<label>Last Car Activity</label>
<description>Last time either the engine was on or the car was charging</description>
</channel>
<channel id="last-position-update" typeId="timestamp-channel">
<label>Last Position Timestamp</label>
<description>Last time the Position data was updated</description>
</channel>
<channel id="last-charge-state-update" typeId="timestamp-channel">
<label>Last Charge State Timestamp</label>
<description>Last time the Charge State data was updated</description>
</channel>
<channel id="remote-ac-status" typeId="remote-ac-status-channel"/>
<channel id="switch-ac" typeId="switch-ac-channel"/>
<channel id="force-refresh" typeId="force-refresh-channel"/>
<channel id="last-alarm-message-date" typeId="timestamp-channel">
<label>Last Alarm Message Timestamp</label>
<description>Last time an alarm message was sent</description>
</channel>
<channel id="last-alarm-message-content" typeId="alarm-message-channel"/>
</channels>
<representation-property>vin</representation-property>
<config-description-ref uri="thing-type:saicismart:vehicle"/>
</thing-type>
<channel-type id="odometer-channel">
<item-type>Number:Length</item-type>
<label>Total Distance Driven</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="range-electric-channel">
<item-type>Number:Length</item-type>
<label>Electric Range</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="power-channel">
<item-type>Number:Power</item-type>
<label>Power Usage</label>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="charging-channel">
<item-type>Switch</item-type>
<label>Charging</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="engine-channel">
<item-type>Switch</item-type>
<label>Engine State</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="tyre-pressure-channel">
<item-type>Number:Pressure</item-type>
<label>Pressure</label>
<category>Pressure</category>
<tags>
<tag>Measurement</tag>
<tag>Pressure</tag>
</tags>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="temperature-channel">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="speed-channel">
<item-type>Number:Speed</item-type>
<label>Speed</label>
<description>Vehicle speed</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="location-channel">
<item-type>Location</item-type>
<label>Location</label>
<description>The actual position of the vehicle</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="heading-channel">
<item-type>Number:Angle</item-type>
<label>Heading</label>
<description>Indicates the (compass) heading of the car, in 0-360 degrees</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="auxiliary-battery-voltage-channel">
<item-type>Number:ElectricPotential</item-type>
<label>Auxiliary Battery Voltage</label>
<description>Voltage (V) of the auxiliary battery</description>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.1f V" readOnly="true"/>
</channel-type>
<channel-type id="door-channel">
<item-type>Contact</item-type>
<label>Door</label>
<description>Indicates if the door is opened</description>
<category>door</category>
<tags>
<tag>OpenState</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="window-channel">
<item-type>Contact</item-type>
<label>Window</label>
<description>Indicates if the window is opened</description>
<category>window</category>
<tags>
<tag>OpenState</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="timestamp-channel">
<item-type>DateTime</item-type>
<label>Timestamp</label>
<description>The time of the event</description>
<state readOnly="true" pattern="%1$tF %1$tR"/>
</channel-type>
<channel-type id="remote-ac-status-channel">
<item-type>Number</item-type>
<label>Remote A/C</label>
<description>Status of remote A/C</description>
<state readOnly="true">
<options>
<option value="0">Off</option>
<option value="5">On</option>
</options>
</state>
</channel-type>
<channel-type id="switch-ac-channel">
<item-type>Switch</item-type>
<label>Switch A/C</label>
<description>Control the A/C remotely</description>
</channel-type>
<channel-type id="force-refresh-channel" advanced="true">
<item-type>Switch</item-type>
<label>Force Refresh</label>
<description>Force an immediate refresh of the car data</description>
</channel-type>
<channel-type id="alarm-message-channel">
<item-type>String</item-type>
<label>Vehicle Message</label>
<description>Vehicle Message</description>
</channel-type>
</thing:thing-descriptions>

View File

@ -338,6 +338,7 @@
<module>org.openhab.binding.rotel</module>
<module>org.openhab.binding.russound</module>
<module>org.openhab.binding.sagercaster</module>
<module>org.openhab.binding.saicismart</module>
<module>org.openhab.binding.samsungtv</module>
<module>org.openhab.binding.satel</module>
<module>org.openhab.binding.semsportal</module>