From a1e6a4e35c51e9ee902edf27200a4d13e98f35ff Mon Sep 17 00:00:00 2001 From: Kai Kreuzer Date: Thu, 13 Jan 2022 09:04:24 +0100 Subject: [PATCH] [tesla] Add support for setting charging amps and sentry mode (#12029) * Add support for setting charging amps and sentry mode Signed-off-by: Kai Kreuzer --- bundles/org.openhab.binding.tesla/README.md | 27 ++++++------ .../tesla/internal/TeslaBindingConstants.java | 2 + .../internal/TeslaChannelSelectorProxy.java | 25 +++++++++++ .../internal/handler/TeslaAccountHandler.java | 2 +- .../internal/handler/TeslaVehicleHandler.java | 42 +++++++++++++++++++ .../tesla/internal/protocol/ChargeState.java | 1 + .../resources/OH-INF/i18n/tesla.properties | 14 +++++-- .../main/resources/OH-INF/thing/channels.xml | 27 +++++++++--- .../main/resources/OH-INF/thing/model3.xml | 3 ++ .../main/resources/OH-INF/thing/models.xml | 3 ++ .../main/resources/OH-INF/thing/modelx.xml | 3 ++ .../main/resources/OH-INF/thing/modely.xml | 3 ++ 12 files changed, 130 insertions(+), 22 deletions(-) diff --git a/bundles/org.openhab.binding.tesla/README.md b/bundles/org.openhab.binding.tesla/README.md index 5265e45c89..49bac12972 100644 --- a/bundles/org.openhab.binding.tesla/README.md +++ b/bundles/org.openhab.binding.tesla/README.md @@ -110,21 +110,22 @@ Additionally, these advanced channels are available (not all are available on al | chargelimit | Dimmer | Charge Limit | Limit charging of the vehicle to the given % | | chargelimitmaximum | Dimmer | Charge Limit Maximum | Maximum charging limit of the vehicle, as % | | chargelimitminimum | Dimmer | Charge Limit Minimum | Minimum charging limit of the vehicle, as % | -| chargelimitsocstandard | Dimmer | Charge Limit SOC Standard | Standard charging limit of the vehicle, in % | -| chargeidealdistanceadded | Number:Length | "Ideal" Charge Distance Added | "Ideal" range added during the last charging session | +| chargelimitsocstandard | Dimmer | Charge Limit SOC Standard | Standard charging limit of the vehicle, in % | +| chargeidealdistanceadded | Number:Length | Ideal Charge Distance Added | Ideal range added during the last charging session | | chargemaxcurrent | Number:ElectricCurrent | Charge Max Current | Maximum current (Ampere) that can be requested from the charger | -| chargerateddistanceadded | Number:Length | "Rated" Charge Distance Added | "Rated" range added during the last charging session | +| chargerateddistanceadded | Number:Length | Rated Charge Distance Added | Rated range added during the last charging session | | chargerate | Number:Speed | Charge Rate | Distance per hour charging rate | | chargestartingrange | String | Charge Starting Range | Undocumented / To be defined | | chargestartingsoc | String | Charge Starting SOC | Undocumented / To be defined | -| chargetomax | Switch | Charge To Max Range | Indicates if charging to the maximum range is enabled | +| chargetomax | Switch | Charge To Max Range | Indicates if charging to the maximum range is enabled | | chargercurrent | Number:ElectricCurrent | Charge Current | Current (Ampere) actually being drawn from the charger | | chargerphases | Number | Charger Phases | Indicates the number of phases (1 to 3) used for charging | | chargermaxcurrent | Number:ElectricCurrent | Charger Maximum Current | Maximum current (Ampere) that can be delivered by the charger | | chargerpower | Number | Charger Power | Power actually delivered by the charger | | chargervoltage | Number:ElectricPotential | Charger Voltage | Voltage (V) actually presented by the charger | -| driverfrontdoor | Contact | Driver Front Door | Indicates if the front door at the driver's side is open | -| driverreardoor | Contact | Driver Rear Door | Indicates if the rear door at the driver's side is open | +| chargingamps | Number:ElectricCurrent | Set Charging Amps | Current (Ampere) to use for charging | +| driverfrontdoor | Contact | Driver Front Door | Indicates if the front door at the driver's side is open | +| driverreardoor | Contact | Driver Rear Door | Indicates if the rear door at the driver's side is open | | drivertemp | Number:Temperature | Driver Temperature | Indicates the auto conditioning temperature set at the driver's side | | eventstamp | DateTime | Event Timestamp | Timestamp of the last event received from the Tesla streaming service | | estimatedbatteryrange | Number:Length | Estimated Battery Range | Estimated battery range | @@ -138,21 +139,21 @@ Additionally, these advanced channels are available (not all are available on al | headingestimation | Number | Estimated Heading | Estimated (compass) heading of the car, in 0 to 360 degrees | | honkhorn | Switch | Honk the Horn | Honk the horn of the vehicle, when ON is received | | homelink | Switch | Homelink Nearby | Indicates if the Home Link is nearby | -| idealbatteryrange | Number:Length | Ideal Battery Range | Indicates the Battery Range | +| idealbatteryrange | Number:Length | Ideal Battery Range | Indicates the Battery Range | | lefttempdirection | Number | Left Temperature Direction | Not documented / To be defined | | lastautoparkerror | String | Last Autopark Error | Not documented / To be defined | | location" advanced="false | Location | Location | The actual position of the vehicle | | leftseatheater | Switch | Left Seat Heater | Indicates if the left seat heater is switched on | | leftrearseatheater | Switch | Left Rear Seat Heater | Indicates if the left rear seat heater is switched on | -| leftrearbackseatheater | Number | Left Rear Backseat Heater | Indicates the level (0, 1, 2, or 3) of the left rear backseat heater | -| managedcharging | Switch | Managed Charging | Indicates if managed charging is active | -| managedchargingcancelled | Switch | Managed Charging Cancelled | Indicates if managed charging is cancelled by the user | +| leftrearbackseatheater | Number | Left Rear Backseat Heater | Indicates the level (0, 1, 2, or 3) of the left rear backseat heater | +| managedcharging | Switch | Managed Charging | Indicates if managed charging is active | +| managedchargingcancelled | Switch | Managed Charging Cancelled | Indicates if managed charging is cancelled by the user | | managedchargingstart | String | Managed Charging Start Time | Not documented / To be defined | | maxcharges | Number | Max Charges | Indicates the number of consecutive "Max Range Charges" performed by the vehicle | | minavailabletemp | Number:Temperature | Minimum Temperature | Indicates the minimal inside temperature of the vehicle | | maxavailabletemp | Number:Temperature | Maximum Temperature | Indicates the maximum inside temperature of the vehicle | | mobileenabled | Switch | Mobile Enabled | Indicates whether the vehicle can be remotely controlled | -| notenoughpower | Switch | Not Enough Power | Indicates if not enough power (ON) is available to heat the vehicle | +| notenoughpower | Switch | Not Enough Power | Indicates if not enough power (ON) is available to heat the vehicle | | notificationsenabled | Switch | Notifications Enabled | Not documented / To be defined | | notificationssupported | Switch | Notifications Supported | Not documented / To be defined | | outsidetemp | Number:Temperature | Outside Temperature | Indicates the outside temperature of the vehicle | @@ -170,10 +171,12 @@ Additionally, these advanced channels are available (not all are available on al | remotestartsupported | Switch | Remote Start Supported | Not documented / To be defined | | rightseatheater | Switch | Right Seat Heater | Indicates if the right seat heater is switched on | | rightrearseatheater | Switch | Right Rear Seat Heater | Indicates if the right rear seat heater is switched on | -| rightrearbackseatheater | Number | Right Rear Backseat Heater | Indicates the level (0, 1, 2, or 3) of the right rear backseat heater | +| rightrearbackseatheater | Number | Right Rear Backseat Heater | Indicates the level (0, 1, 2, or 3) of the right rear backseat heater | | righttempdirection | Number | Right Temperature Direction | Not documented / To be defined | | scheduledchargingpending | Switch | Scheduled Charging Pending | Indicates if a scheduled charging session is still pending | | scheduledchargingstart | DateTime | Scheduled Charging Start | Indicates when the scheduled charging session will start, in yyyy-MM-dd'T'HH:mm:ss format | +| sentrymode | Switch | Sentry Mode | Activates or deactivates sentry mode | +| sentrymodeavailable | Switch | Sentry Mode Available | Indicates whether sentry mode is currently available | | shiftstate | String | Shift State | Indicates the state of the transmission, “P”, “D”, “R”, or “N” | | sidemirrorheaters | Switch | Side Mirror Heaters | Indicates if the side mirror heaters are switched on | | smartpreconditioning | Switch | Smart Preconditioning | Indicates if smart preconditioning is switched on | diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java index 9340c33637..254dc508e3 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java @@ -60,7 +60,9 @@ public class TeslaBindingConstants { public static final String COMMAND_HONK_HORN = "honk_horn"; public static final String COMMAND_OPEN_CHARGE_PORT = "charge_port_door_open"; public static final String COMMAND_RESET_VALET_PIN = "reset_valet_pin"; + public static final String COMMAND_SET_CHARGING_AMPS = "set_charging_amps"; public static final String COMMAND_SET_CHARGE_LIMIT = "set_charge_limit"; + public static final String COMMAND_SET_SENTRY_MODE = "set_sentry_mode"; public static final String COMMAND_SET_TEMP = "set_temps"; public static final String COMMAND_SET_VALET_MODE = "set_valet_mode"; public static final String COMMAND_SUN_ROOF = "sun_roof_control"; diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaChannelSelectorProxy.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaChannelSelectorProxy.java index 0e2bb8884e..ede61d1767 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaChannelSelectorProxy.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaChannelSelectorProxy.java @@ -177,6 +177,7 @@ public class TeslaChannelSelectorProxy { return new QuantityType<>(value, ImperialUnits.MILES_PER_HOUR); } }, + CHARGE_AMPS("charge_amps", "chargingamps", DecimalType.class, false), CHARGE_STARTING_RANGE("charge_starting_range", "chargestartingrange", StringType.class, false), CHARGE_STARTING_SOC("charge_starting_soc", "chargestartingsoc", StringType.class, false), CHARGE_STATE("charging_state", "chargingstate", StringType.class, false), @@ -840,6 +841,30 @@ public class TeslaChannelSelectorProxy { return super.getState(dateFormatter.format(date)); } }, + SENTRY_MODE("sentry_mode", "sentrymode", OnOffType.class, false) { + @Override + public State getState(String s, TeslaChannelSelectorProxy proxy, Map properties) { + if (s.equals("true") || s.equals("1")) { + return super.getState("ON"); + } + if (s.equals("false") || s.equals("0")) { + return super.getState("OFF"); + } + return super.getState(s); + } + }, + SENTRY_MODE_AVAILABLE("sentry_mode_available", "sentrymodeavailable", OnOffType.class, false) { + @Override + public State getState(String s, TeslaChannelSelectorProxy proxy, Map properties) { + if (s.equals("true") || s.equals("1")) { + return super.getState("ON"); + } + if (s.equals("false") || s.equals("0")) { + return super.getState("OFF"); + } + return super.getState(s); + } + }, SHIFTSTATE("shift_state", "shiftstate", StringType.class, false), SIDEMIRROR_HEATING("side_mirror_heaters", "sidemirrorheaters", OnOffType.class, false) { @Override diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java index 6dc54226db..f7ac9fbc13 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java @@ -344,7 +344,7 @@ public class TeslaAccountHandler extends BaseBridgeHandler { try { JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject(); - logger.trace("Request : {}:{}:{} yields {}", command, payLoad, target, jsonObject.get("response")); + logger.trace("Request : {}:{} yields {}", command, payLoad, jsonObject.get("response")); return jsonObject.get("response").toString(); } catch (Exception e) { logger.error("An exception occurred while invoking a REST request: '{}'", e.getMessage()); diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java index 9178580216..c557d61a5e 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java @@ -54,12 +54,14 @@ import org.openhab.binding.tesla.internal.protocol.Vehicle; import org.openhab.binding.tesla.internal.protocol.VehicleState; import org.openhab.binding.tesla.internal.throttler.QueueChannelThrottler; import org.openhab.binding.tesla.internal.throttler.Rate; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -263,6 +265,26 @@ public class TeslaVehicleHandler extends BaseThingHandler { } break; } + case CHARGE_AMPS: + Integer amps = null; + if (command instanceof DecimalType) { + amps = ((DecimalType) command).intValue(); + } + if (command instanceof QuantityType) { + QuantityType qamps = ((QuantityType) command).toUnit(Units.AMPERE); + if (qamps != null) { + amps = qamps.intValue(); + } + } + if (amps != null) { + if (amps < 5 || amps > 32) { + logger.warn("Charging amps can only be set in a range of 5-32A, but not to {}A.", + amps); + return; + } + setChargingAmps(amps); + } + break; case COMBINED_TEMP: { QuantityType quantity = commandToQuantityType(command); if (quantity != null) { @@ -284,6 +306,12 @@ public class TeslaVehicleHandler extends BaseThingHandler { } break; } + case SENTRY_MODE: { + if (command instanceof OnOffType) { + setSentryMode(command == OnOffType.ON); + } + break; + } case SUN_ROOF_STATE: { if (command instanceof StringType) { setSunroof(command.toString()); @@ -565,6 +593,20 @@ public class TeslaVehicleHandler extends BaseThingHandler { requestData(CHARGE_STATE); } + public void setChargingAmps(int amps) { + JsonObject payloadObject = new JsonObject(); + payloadObject.addProperty("charging_amps", amps); + sendCommand(COMMAND_SET_CHARGING_AMPS, gson.toJson(payloadObject), account.commandTarget); + requestData(CHARGE_STATE); + } + + public void setSentryMode(boolean b) { + JsonObject payloadObject = new JsonObject(); + payloadObject.addProperty("on", b); + sendCommand(COMMAND_SET_SENTRY_MODE, gson.toJson(payloadObject), account.commandTarget); + requestData(VEHICLE_STATE); + } + public void setSunroof(String state) { if (state.equals("vent") || state.equals("close")) { JsonObject payloadObject = new JsonObject(); diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ChargeState.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ChargeState.java index 8f4fc8c86c..98f0988c6f 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ChargeState.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/ChargeState.java @@ -41,6 +41,7 @@ public class ChargeState { public float ideal_battery_range; public float time_to_full_charge; public int battery_level; + public int charge_amps; public int charge_current_request; public int charge_current_request_max; public int charge_limit_soc; diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/i18n/tesla.properties b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/i18n/tesla.properties index 1bd21f1fbd..44bf04cc92 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/i18n/tesla.properties +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/i18n/tesla.properties @@ -92,8 +92,8 @@ channel-type.tesla.chargeenablerequest.label = Charge Enable Request channel-type.tesla.chargeenablerequest.description = Undocumented / To be defined channel-type.tesla.chargeenergyadded.label = Charge Energy Added channel-type.tesla.chargeenergyadded.description = Energy added, in kWh, during the last charging session -channel-type.tesla.chargeidealdistanceadded.label = "Ideal" Charge Distance Added -channel-type.tesla.chargeidealdistanceadded.description = "Ideal" range added during the last charging session +channel-type.tesla.chargeidealdistanceadded.label = Ideal Charge Distance Added +channel-type.tesla.chargeidealdistanceadded.description = Ideal range added during the last charging session channel-type.tesla.chargelimit.label = Charge Limit channel-type.tesla.chargelimit.description = Limit charging of the vehicle to the given % channel-type.tesla.chargelimitmaximum.label = Charge Limit Maximum @@ -108,8 +108,8 @@ channel-type.tesla.chargeport.label = Charge Port channel-type.tesla.chargeport.description = Open the Charge Port (ON) or indicates the state of the Charge Port (ON/OFF if Open/Closed) channel-type.tesla.chargerate.label = Charge Rate channel-type.tesla.chargerate.description = Distance per hour charging rate -channel-type.tesla.chargerateddistanceadded.label = "Rated" Charge Distance Added -channel-type.tesla.chargerateddistanceadded.description = "Rated" range added during the last charging session +channel-type.tesla.chargerateddistanceadded.label = Rated Charge Distance Added +channel-type.tesla.chargerateddistanceadded.description = Rated range added during the last charging session channel-type.tesla.chargercurrent.label = Charge Current channel-type.tesla.chargercurrent.description = Current (Ampere) actually being drawn from the charger channel-type.tesla.chargermaxcurrent.label = Charger Maximum Current @@ -126,6 +126,8 @@ channel-type.tesla.chargestartingsoc.label = Charge Starting SOC channel-type.tesla.chargestartingsoc.description = Undocumented / To be defined channel-type.tesla.chargetomax.label = Charge To Max Range channel-type.tesla.chargetomax.description = Indicates if the charging to the maximum range is enabled +channel-type.tesla.chargingamps.label = Set Charging Amps +channel-type.tesla.chargingamps.description = Current (Ampere) to use for charging channel-type.tesla.chargingstate.label = Charging State channel-type.tesla.chargingstate.description = “Starting”, “Complete”, “Charging”, “Disconnected”, “Stopped”, “NoPower” channel-type.tesla.chargingstate.state.option.Starting = Starting @@ -246,6 +248,10 @@ channel-type.tesla.scheduledchargingpending.label = Scheduled Charging Pending channel-type.tesla.scheduledchargingpending.description = Indicates if a scheduled charging session is still pending channel-type.tesla.scheduledchargingstart.label = Scheduled Charging Start channel-type.tesla.scheduledchargingstart.description = Indicates when the scheduled charging session will start, in yyyy-MM-dd'T'HH:mm:ss format +channel-type.tesla.sentrymode.label = Sentry Mode +channel-type.tesla.sentrymode.description = Activates or deactivates sentry mode +channel-type.tesla.sentrymodeavailable.label = Sentry Available +channel-type.tesla.sentrymodeavailable.description = Indicates whether sentry mode is currently available channel-type.tesla.shiftstate.label = Shift State channel-type.tesla.shiftstate.description = Indicates the state of the transmission, “P”, “D”, “R”, or “N” channel-type.tesla.sidemirrorheaters.label = Side Mirror Heaters diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/channels.xml index abf97d9dad..f9ab8be21c 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/channels.xml @@ -92,6 +92,12 @@ Undocumented / To be defined + + Number:ElectricCurrent + + Current (Ampere) to use for charging + + Number:ElectricCurrent @@ -136,8 +142,8 @@ Number:Length - - "Ideal" range added during the last charging session + + Ideal range added during the last charging session @@ -148,8 +154,8 @@ Number:Length - - "Rated" range added during the last charging session + + Rated range added during the last charging session @@ -415,7 +421,7 @@ Switch - + Indicates if not enough power (ON) is available to heat the vehicle @@ -550,6 +556,17 @@ Indicates when the scheduled charging session will start, in yyyy-MM-dd'T'HH:mm:ss format + + Switch + + Activates or deactivates sentry mode + + + Switch + + Indicates whether sentry mode is currently available + + String diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/model3.xml b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/model3.xml index 1ceca7a841..0566a8eee1 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/model3.xml +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/model3.xml @@ -26,6 +26,7 @@ + @@ -100,6 +101,8 @@ + + diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/models.xml b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/models.xml index 7d394f8660..622a27b080 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/models.xml +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/models.xml @@ -28,6 +28,7 @@ + @@ -103,6 +104,8 @@ + + diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modelx.xml b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modelx.xml index df139b3a2a..4087941fa3 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modelx.xml +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modelx.xml @@ -28,6 +28,7 @@ + @@ -103,6 +104,8 @@ + + diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modely.xml b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modely.xml index 61fda3be99..7724139c35 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modely.xml +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modely.xml @@ -27,6 +27,7 @@ + @@ -102,6 +103,8 @@ + + -- 2.47.3