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 b4d76d97e3d..ae3e71b935f 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.sbus