Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,20 @@ The key-value pairs in the JSON express the following:
| chargeStateTopic | topic indicating the charge state - **required** |
| chargingValue | payload that indicates the charging - **required** |
| socTopic | topic where the gateway publishes the SoC for the charging station - optional |
| socTsTopic | topic where the gateway publishes the SoC timestamp for the charging station - optional |
| rangeTopic | topic where the gateway publishes the range for the charging station - optional |
| chargerConnectedTopic | topic indicating that the vehicle is connected to the charging station - optional |
| chargerConnectedValue | payload that indicates that the charger is connected - optional |
| vin | vehicle identification number to map the charging station information to a vehicle - **required** |
­| importedEnergyTopic | topic where the charging station publishes the amount of imported energy (in Wh) - optional |

#### `importedEnergyTopic`

This topic provides the amount of energy imported by the charging station, measured in watt-hours.

During the charging process, the system normally calculates the vehicle status refresh interval based on the current charging power and the vehicle’s battery capacity. However, this estimation can become inaccurate during PV surplus charging, where the charging power fluctuates significantly.

If the charging station can report the total imported energy (for example, the `daily_imported` topic provided by openWB), this value can be used instead to determine when to refresh the vehicle status. This approach ensures more reliable updates even under variable charging conditions.

### Advanced settings

Expand Down
12 changes: 8 additions & 4 deletions examples/charging-stations.json.sample_openWB_2.0
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@
{
"chargeStateTopic": "openWB/chargepoint/2/get/charge_state",
"chargingValue": "true",
"socTopic": "openWB/set/vehicle/2/get/soc",
"rangeTopic": "openWB/set/vehicle/2/get/range",
"socTopic": "openWB/set/mqtt/vehicle/2/get/soc",
"socTsTopic": "openWB/set/mqtt/vehicle/2/get/soc_timestamp",
"rangeTopic": "openWB/set/mqtt/vehicle/2/get/range",
"chargerConnectedTopic": "openWB/chargepoint/2/get/plug_state",
"chargerConnectedValue": "true",
"importedEnergyTopic": "openWB/chargepoint/2/get/daily_imported",
"vin": "vin1"
},
{
"chargeStateTopic": "openWB/chargepoint/3/get/charge_state",
"chargingValue": "true",
"socTopic": "openWB/set/vehicle/3/get/soc",
"rangeTopic": "openWB/set/vehicle/3/get/range",
"socTopic": "openWB/set/mqtt/vehicle/3/get/soc",
"socTsTopic": "openWB/set/mqtt/vehicle/3/get/soc_timestamp",
"rangeTopic": "openWB/set/mqtt/vehicle/3/get/range",
"chargerConnectedTopic": "openWB/chargepoint/3/get/plug_state",
"chargerConnectedValue": "true",
"importedEnergyTopic": "openWB/chargepoint/2/get/daily_imported",
"vin": "vin2"
}
]
46 changes: 44 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pytest-asyncio = "^1.2.0"
pytest-mock = "^3.14.0"
mypy = "^1.15.0"
pylint = "^3.3.6"
freezegun = "^1.5.5"

[tool.poetry.dependencies]
saic-ismart-client-ng = { develop = true }
Expand Down
19 changes: 11 additions & 8 deletions src/configuration/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,21 +550,24 @@ def __process_charging_stations_file(config: Configuration, json_file: str) -> N
for item in data:
charge_state_topic = item["chargeStateTopic"]
charging_value = item["chargingValue"]
soc_topic = item.get("socTopic", None)
soc_ts_topic = item.get("socTsTopic", None)
vin = item["vin"]
if "socTopic" in item:
charging_station = ChargingStation(
vin, charge_state_topic, charging_value, item["socTopic"]
)
else:
charging_station = ChargingStation(
vin, charge_state_topic, charging_value
)
charging_station = ChargingStation(
vin,
charge_state_topic,
charging_value,
soc_topic,
soc_ts_topic,
)
if "rangeTopic" in item:
charging_station.range_topic = item["rangeTopic"]
if "chargerConnectedTopic" in item:
charging_station.connected_topic = item["chargerConnectedTopic"]
if "chargerConnectedValue" in item:
charging_station.connected_value = item["chargerConnectedValue"]
if "importedEnergyTopic" in item:
charging_station.imported_energy_topic = item["importedEnergyTopic"]
config.charging_stations_by_vin[vin] = charging_station
except FileNotFoundError:
LOG.warning(f"File {json_file} does not exist")
Expand Down
13 changes: 13 additions & 0 deletions src/handlers/vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import mqtt_topics
from saic_api_listener import MqttGatewayAbrpListener, MqttGatewayOsmAndListener
from status_publisher.vehicle_info import VehicleInfoPublisher
from vehicle import RefreshMode

if TYPE_CHECKING:
from saic_ismart_client_ng import SaicApi
Expand Down Expand Up @@ -308,6 +309,18 @@ def __setup_ha_discovery(
return HomeAssistantDiscovery(vehicle_state, vin_info, config)
return None

async def handle_charging_station_energy_imported(self, energy: float) -> None:
if self.openwb_integration is None:
return
if self.openwb_integration.should_refresh_by_imported_energy(
energy, self.vehicle_state.charge_polling_min_percent, self.vin_info.vin
):
LOG.info(
f"Triggering refresh due to imported energy {energy} Wh for vehicle {self.vin_info.vin}"
)
self.vehicle_state.set_refresh_mode(
RefreshMode.FORCE, "imported energy threshold reached"
)

class VehicleHandlerLocator(ABC):
def get_vehicle_handler(self, vin: str) -> VehicleHandler | None:
Expand Down
93 changes: 92 additions & 1 deletion src/integrations/openwb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import datetime
import logging
import math
from typing import TYPE_CHECKING

import extractors
Expand Down Expand Up @@ -29,18 +31,24 @@ def __init__(
) -> None:
self.__charging_station = charging_station
self.__publisher = publisher
self.charger_connected: bool | None = None
self.real_total_battery_capacity: float | None = None
self.last_imported_energy_wh: float | None = None
self.computed_refresh_by_imported_energy_wh: float | None = None

def update_openwb(
self,
vehicle_status: VehicleStatusRespProcessingResult,
charge_status: ChrgMgmtDataRespProcessingResult | None,
) -> None:
if charge_status:
self.real_total_battery_capacity = charge_status.real_total_battery_capacity
range_topic = self.__charging_station.range_topic
electric_range = extractors.extract_electric_range(
vehicle_status, charge_status
)
if electric_range is not None and range_topic is not None:
LOG.info("OpenWB Integration published range to %f", range_topic)
LOG.info("OpenWB Integration published range to %s", range_topic)
self.__publisher.publish_float(
key=range_topic,
value=electric_range,
Expand All @@ -56,3 +64,86 @@ def update_openwb(
value=soc,
no_prefix=True,
)

soc_ts_topic = self.__charging_station.soc_ts_topic
soc_ts = int(datetime.datetime.now().timestamp())
if soc_ts_topic is not None:
LOG.info("OpenWB Integration published SoC timestamp to %s", soc_ts_topic)
self.__publisher.publish_int(
key=soc_ts_topic,
value=soc_ts,
no_prefix=True,
)

def set_charger_connection_state(self, connected: bool, vin: str) -> None:
if self.charger_connected == connected:
# No change, do nothing
return

self.charger_connected = connected
if self.charger_connected:
LOG.info("OpenWB Integration: Charger connected to vehicle %s", vin)
else:
LOG.info("OpenWB Integration: Charger disconnected from vehicle %s", vin)

def should_refresh_by_imported_energy(
self, imported_energy_wh: float, charge_polling_min_percent: float, vin: str
) -> bool:
"""Determine if the vehicle status should be refreshed based on imported energy.

The method triggers a refresh if the imported energy since the last refresh
exceeds a threshold based on battery capacity and a minimum percentage.
If the imported energy decreases (e.g., daily reset), the threshold is recalculated.
"""
# Charger connection state might not be available. If disconnected, skip the check.
if self.charger_connected is False:
LOG.debug(
f"Charger for vehicle {vin} is disconnected. Skipping imported energy check."
)
return False
# Return False if battery capacity is not available
if self.real_total_battery_capacity is None:
LOG.warning(
"Battery capacity not available. Cannot calculate energy per percent."
)
return False

# Calculate the energy corresponding to 1% of the battery in Wh
energy_per_percent = (self.real_total_battery_capacity * 1000) / 100.0

# Minimum energy threshold for triggering a refresh
energy_for_min_pct = math.ceil(charge_polling_min_percent * energy_per_percent)

# Initialize the refresh threshold if it hasn't been set yet
if not self.computed_refresh_by_imported_energy_wh or imported_energy_wh < (
self.last_imported_energy_wh or 0
):
self.computed_refresh_by_imported_energy_wh = (
imported_energy_wh + energy_for_min_pct
)
LOG.debug(
f"Initial or reset threshold for vehicle {vin} set to "
f"{self.computed_refresh_by_imported_energy_wh} Wh"
)

# Check if the imported energy exceeds the threshold
refresh_needed = False
if imported_energy_wh >= self.computed_refresh_by_imported_energy_wh:
LOG.info(
f"OpenWB Integration: Imported energy threshold of {self.computed_refresh_by_imported_energy_wh} Wh reached "
f"(current: {imported_energy_wh} Wh). Triggering vehicle refresh."
)
refresh_needed = True

# Calculate the next threshold
self.computed_refresh_by_imported_energy_wh = (
imported_energy_wh + energy_for_min_pct
)
LOG.debug(
f"Next imported energy threshold for vehicle {vin} set to "
f"{self.computed_refresh_by_imported_energy_wh} Wh"
)

# Save the last imported energy value
self.last_imported_energy_wh = imported_energy_wh
return refresh_needed
13 changes: 10 additions & 3 deletions src/integrations/openwb/charging_station.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ def __init__(
charge_state_topic: str,
charging_value: str,
soc_topic: str | None = None,
soc_ts_topic: str | None = None,
range_topic: str | None = None,
connected_topic: str | None = None,
connected_value: str | None = None,
imported_energy_topic: str | None = None,
) -> None:
self.vin: str = vin
self.charge_state_topic: str = charge_state_topic
self.charging_value: str = charging_value
self.soc_topic: str | None = soc_topic
self.range_topic: str | None = None
self.connected_topic: str | None = None
self.connected_value: str | None = None
self.soc_ts_topic: str | None = soc_ts_topic
self.range_topic: str | None = range_topic
self.connected_topic: str | None = connected_topic
self.connected_value: str | None = connected_value
self.imported_energy_topic: str | None = imported_energy_topic
24 changes: 24 additions & 0 deletions src/mqtt_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,30 @@ async def on_charging_detected(self, vin: str) -> None:
else:
LOG.debug(f"Charging detected for unknown vin {vin}")

@override
async def on_charging_station_energy_imported(
self, vin: str, energy: float
) -> None:
vehicle_handler = self.get_vehicle_handler(vin)
if vehicle_handler:
await vehicle_handler.handle_charging_station_energy_imported(energy)
else:
LOG.debug(f"Imported energy for unknown vin {vin} received")

@override
async def on_charger_connection_state_changed(
self, vin: str, connected: bool
) -> None:
vehicle_handler = self.get_vehicle_handler(vin)
if not vehicle_handler:
LOG.debug(f"Connected state change for unknown vin {vin} received")
return

integration = vehicle_handler.openwb_integration
if integration:
integration.set_charger_connection_state(connected, vin)
LOG.debug(f"Updated connected state for vehicle {vin} to {connected}")

@override
async def on_mqtt_global_command_received(
self, *, topic: str, payload: str
Expand Down
11 changes: 11 additions & 0 deletions src/publisher/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ async def on_mqtt_global_command_received(
) -> None:
raise NotImplementedError("Should have implemented this")

@abstractmethod
async def on_charging_station_energy_imported(
self, vin: str, energy: float
) -> None:
raise NotImplementedError("Should have implemented this")
@abstractmethod
async def on_charger_connection_state_changed(
self, vin: str, connected: bool
) -> None:
raise NotImplementedError("Should have implemented this")


class Publisher(ABC):
def __init__(self, config: Configuration) -> None:
Expand Down
Loading