From 652845fee54b6a20b40b06882976e0362ec7c9cb Mon Sep 17 00:00:00 2001 From: Doug Culnane <32482395+dougculnane@users.noreply.github.com> Date: Sat, 27 Apr 2024 22:49:34 +0200 Subject: [PATCH] [saicismart] Initial contribution (#15894) * [saicismart] initial binding creation Signed-off-by: Markus Heberling Signed-off-by: dougculnane --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.saicismart/NOTICE | 13 + .../org.openhab.binding.saicismart/README.md | 131 ++++++++ .../org.openhab.binding.saicismart/pom.xml | 52 +++ .../src/main/feature/feature.xml | 9 + .../internal/ChargeStateUpdater.java | 113 +++++++ .../internal/SAICiSMARTBindingConstants.java | 99 ++++++ .../SAICiSMARTBridgeConfiguration.java | 27 ++ .../internal/SAICiSMARTBridgeHandler.java | 297 ++++++++++++++++++ .../internal/SAICiSMARTHandler.java | 283 +++++++++++++++++ .../internal/SAICiSMARTHandlerFactory.java | 75 +++++ .../SAICiSMARTVehicleConfiguration.java | 30 ++ .../saicismart/internal/VehicleDiscovery.java | 81 +++++ .../internal/VehicleStateUpdater.java | 242 ++++++++++++++ .../ChargingStatusAPIException.java | 28 ++ .../exceptions/VehicleStatusAPIException.java | 28 ++ .../src/main/resources/OH-INF/addon/addon.xml | 9 + .../resources/OH-INF/config/bridge-config.xml | 18 ++ .../resources/OH-INF/config/thing-config.xml | 17 + .../resources/OH-INF/thing/bridge-ismart.xml | 12 + .../resources/OH-INF/thing/thing-types.xml | 229 ++++++++++++++ bundles/pom.xml | 1 + 23 files changed, 1800 insertions(+) create mode 100644 bundles/org.openhab.binding.saicismart/NOTICE create mode 100644 bundles/org.openhab.binding.saicismart/README.md create mode 100644 bundles/org.openhab.binding.saicismart/pom.xml create mode 100644 bundles/org.openhab.binding.saicismart/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/ChargeStateUpdater.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBindingConstants.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBridgeHandler.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTHandler.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTHandlerFactory.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTVehicleConfiguration.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/VehicleDiscovery.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/VehicleStateUpdater.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/exceptions/ChargingStatusAPIException.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/exceptions/VehicleStatusAPIException.java create mode 100644 bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/config/bridge-config.xml create mode 100644 bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/config/thing-config.xml create mode 100644 bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/thing/bridge-ismart.xml create mode 100644 bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index ba7010f42f0..9a61eff6f43 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 3c2c2b5e7a8..96a0264b851 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1516,6 +1516,11 @@ org.openhab.binding.sagercaster ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.saicismart + ${project.version} + org.openhab.addons.bundles org.openhab.binding.samsungtv diff --git a/bundles/org.openhab.binding.saicismart/NOTICE b/bundles/org.openhab.binding.saicismart/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.saicismart/README.md b/bundles/org.openhab.binding.saicismart/README.md new file mode 100644 index 00000000000..a86c0d4ed28 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/README.md @@ -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. diff --git a/bundles/org.openhab.binding.saicismart/pom.xml b/bundles/org.openhab.binding.saicismart/pom.xml new file mode 100644 index 00000000000..f77b2686bff --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/pom.xml @@ -0,0 +1,52 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.binding.saicismart + + openHAB Add-ons :: Bundles :: SAICiSMART Binding + + org.brotli.dec;resolution:=optional,org.conscrypt;resolution:=optional + + + + + io.github.saic-ismart-api + saic-ismart-client + 0.3.0 + + + io.github.saic-ismart-api + saic-ismart-api + 0.3.0 + + + org.apache.httpcomponents.client5 + httpclient5 + 5.2.1 + + + org.apache.httpcomponents.core5 + httpcore5 + 5.2 + + + org.apache.httpcomponents.core5 + httpcore5-h2 + 5.2 + + + net.heberling.binarynotes + binarynotes + 1.7.0 + + + diff --git a/bundles/org.openhab.binding.saicismart/src/main/feature/feature.xml b/bundles/org.openhab.binding.saicismart/src/main/feature/feature.xml new file mode 100644 index 00000000000..1371384f3b8 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.saicismart/${project.version} + + diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/ChargeStateUpdater.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/ChargeStateUpdater.java new file mode 100644 index 00000000000..ebc3d9a556a --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/ChargeStateUpdater.java @@ -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 { + 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 chargingStatusRequestmessageCoder = new MessageCoder<>( + IASN1PreparedElement.class); + Message 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 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(); + } +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBindingConstants.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBindingConstants.java new file mode 100644 index 00000000000..87532c86e8d --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBindingConstants.java @@ -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"; +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBridgeConfiguration.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBridgeConfiguration.java new file mode 100644 index 00000000000..97d1eb56f37 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBridgeConfiguration.java @@ -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 = ""; +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBridgeHandler.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBridgeHandler.java new file mode 100644 index 00000000000..2b9d564ca0d --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTBridgeHandler.java @@ -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 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 mpUserLoggingInRequestMessageCoder = new MessageCoder<>( + MP_UserLoggingInReq.class); + + MP_UserLoggingInReq mpUserLoggingInReq = new MP_UserLoggingInReq(); + mpUserLoggingInReq.setPassword(config.password); + Message 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 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 messageListReqMessageCoder = new MessageCoder<>(MessageListReq.class); + Message 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 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 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 alarmSwitchMessage = alarmSwitchReqMessageCoder.initializeMessage(uid, token, null, + "521", 513, 1, alarmSwitchReq); + String alarmSwitchRequest = alarmSwitchReqMessageCoder.encodeRequest(alarmSwitchMessage); + String alarmSwitchResponse = sendRequest(alarmSwitchRequest, API_ENDPOINT_V11); + final MessageCoder alarmSwitchResMessageCoder = new MessageCoder<>( + IASN1PreparedElement.class); + Message 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> getServices() { + return Collections.singleton(VehicleDiscovery.class); + } + + @Nullable + public String getUid() { + return uid; + } + + @Nullable + public String getToken() { + return token; + } + + public Collection 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; + } + } +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTHandler.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTHandler.java new file mode 100644 index 00000000000..6d9f08c6069 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTHandler.java @@ -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 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 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 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 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(); + } +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTHandlerFactory.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTHandlerFactory.java new file mode 100644 index 00000000000..abaf4d9c526 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTHandlerFactory.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTVehicleConfiguration.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTVehicleConfiguration.java new file mode 100644 index 00000000000..478d55df7a7 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/SAICiSMARTVehicleConfiguration.java @@ -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; +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/VehicleDiscovery.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/VehicleDiscovery.java new file mode 100644 index 00000000000..5ef6c3fa437 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/VehicleDiscovery.java @@ -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 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 configProperties) { + super.activate(configProperties); + } + + @Override + public void deactivate() { + super.deactivate(); + } +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/VehicleStateUpdater.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/VehicleStateUpdater.java new file mode 100644 index 00000000000..2a66e3ec9f1 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/VehicleStateUpdater.java @@ -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 { + 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 otaRvmVehicleStatusRequstMessageCoder = new MessageCoder<>( + OTA_RVMVehicleStatusReq.class); + + OTA_RVMVehicleStatusReq otaRvmVehicleStatusReq = new OTA_RVMVehicleStatusReq(); + otaRvmVehicleStatusReq.setVehStatusReqType(1); + + Message 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 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(); + } +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/exceptions/ChargingStatusAPIException.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/exceptions/ChargingStatusAPIException.java new file mode 100644 index 00000000000..c571c60d3ff --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/exceptions/ChargingStatusAPIException.java @@ -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())); + } +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/exceptions/VehicleStatusAPIException.java b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/exceptions/VehicleStatusAPIException.java new file mode 100644 index 00000000000..1e76d00dcc3 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/java/org/openhab/binding/saicismart/internal/exceptions/VehicleStatusAPIException.java @@ -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())); + } +} diff --git a/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..9aa858b705e --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,9 @@ + + + binding + SAICiSMART Binding + This is the binding for SAIC (MG) iSMART Cars. + cloud + diff --git a/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/config/bridge-config.xml new file mode 100644 index 00000000000..801d534d2b4 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/config/bridge-config.xml @@ -0,0 +1,18 @@ + + + + + + + iSMART Username + + + + iSMART Password + password + + + diff --git a/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/config/thing-config.xml b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/config/thing-config.xml new file mode 100644 index 00000000000..1cdb9c757a3 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/config/thing-config.xml @@ -0,0 +1,17 @@ + + + + + + + Unique Vehicle Identification Number (VIN) given by SAIC + + + + User Token for A Better Routeplanner. + + + diff --git a/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/thing/bridge-ismart.xml b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/thing/bridge-ismart.xml new file mode 100644 index 00000000000..c650c1f8c61 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/thing/bridge-ismart.xml @@ -0,0 +1,12 @@ + + + + + + Your iSMART account data + + + diff --git a/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..b1ae62feba4 --- /dev/null +++ b/bundles/org.openhab.binding.saicismart/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,229 @@ + + + + + + + + + + + iSMART enabled car + Car + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Last time either the engine was on or the car was charging + + + + Last time the Position data was updated + + + + Last time the Charge State data was updated + + + + + + + Last time an alarm message was sent + + + + + vin + + + + + + Number:Length + + + + + Number:Length + + + + + Number:Power + + + Measurement + Power + + + + + Switch + + + + + Switch + + + + + Number:Pressure + + Pressure + + Measurement + Pressure + + + + + Number:Temperature + + Temperature + + Measurement + Temperature + + + + + Number:Speed + + Vehicle speed + + + + Location + + The actual position of the vehicle + + + + Number:Angle + + Indicates the (compass) heading of the car, in 0-360 degrees + + + + Number:ElectricPotential + + Voltage (V) of the auxiliary battery + + Measurement + Voltage + + + + + Contact + + Indicates if the door is opened + door + + OpenState + + + + + Contact + + Indicates if the window is opened + window + + OpenState + + + + + DateTime + + The time of the event + + + + Number + + Status of remote A/C + + + + + + + + + Switch + + Control the A/C remotely + + + Switch + + Force an immediate refresh of the car data + + + String + + Vehicle Message + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 9f582c6c111..ee025251325 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -338,6 +338,7 @@ org.openhab.binding.rotel org.openhab.binding.russound org.openhab.binding.sagercaster + org.openhab.binding.saicismart org.openhab.binding.samsungtv org.openhab.binding.satel org.openhab.binding.semsportal