Skip to content

Commit

Permalink
Merge pull request #17 from ad-ha/ad-ha-0.4.2
Browse files Browse the repository at this point in the history
0.4.2
  • Loading branch information
ad-ha authored Nov 7, 2024
2 parents dfb13ec + 57d0c51 commit 62a1521
Show file tree
Hide file tree
Showing 12 changed files with 650 additions and 120 deletions.
21 changes: 21 additions & 0 deletions custom_components/mg_saic/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,27 @@ async def send_vehicle_charging_control(self, vin, action):
LOGGER.error(f"Error sending charging {action} command for VIN {vin}: {e}")
raise

async def send_vehicle_charging_ptc_heat(self, vin, action):
"""Send a charging control command to the vehicle."""
await self._ensure_initialized()
try:
LOGGER.debug(f"Battery heating control - VIN: {vin}, action: {action}")

# Use the control_charging method from the saic-python-client-ng library
if action == "start":
await self.saic_api.control_battery_heating(vin=vin, enable=False)
else:
await self.saic_api.control_battery_heating(vin=vin, enable=True)

LOGGER.info(
f"Battery heating {action} command sent successfully for VIN: {vin}"
)
except Exception as e:
LOGGER.error(
f"Error sending battery heating {action} command for VIN {vin}: {e}"
)
raise

async def control_rear_window_heat(self, vin, action):
"""Control the rear window heat."""
await self._ensure_initialized()
Expand Down
14 changes: 13 additions & 1 deletion custom_components/mg_saic/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ def __init__(self, coordinator, client, vin_info, vin):
self._attr_unique_id = f"{vin}_climate"
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL]
self._attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH]
Expand Down Expand Up @@ -97,9 +100,18 @@ async def async_set_hvac_mode(self, hvac_mode):
self._attr_hvac_mode = HVACMode.COOL
else:
LOGGER.warning("Unsupported HVAC mode: %s", hvac_mode)
return
# Schedule data refresh
await self.coordinator.async_request_refresh()

async def async_turn_on(self):
"""Turn the climate entity on."""
await self.async_set_hvac_mode(HVACMode.COOL)

async def async_turn_off(self):
"""Turn the climate entity off."""
await self.async_set_hvac_mode(HVACMode.OFF)

async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
Expand Down
23 changes: 20 additions & 3 deletions custom_components/mg_saic/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@
# Conversion factors
PRESSURE_TO_BAR = 0.04
DATA_DECIMAL_CORRECTION = 0.1
DATA_DECIMAL_CORRECTION_SOC = 0.1

# Conversion factors for charging data
CHARGING_CURRENT_FACTOR = 0.001
CHARGING_VOLTAGE_FACTOR = 0.145

# API Base Urls
BASE_URLS = {
"EU": "https://gateway-mg-eu.soimt.com",
"China": "https://gateway-mg-china.soimt.com",
"Asia": "https://gateway-mg-asia.soimt.com",
}

# Phone Login Country Codes
COUNTRY_CODES = [
{"code": "+1", "country": "USA"},
{"code": "+7", "country": "Russia"},
Expand Down Expand Up @@ -57,9 +64,19 @@
]

# Update Settings
UPDATE_INTERVAL = timedelta(minutes=120)
UPDATE_INTERVAL_CHARGING = timedelta(minutes=5)
RETRY_LIMIT = 3
DEFAULT_SCAN_INTERVAL = timedelta(minutes=120)
DEFAULT_CHARGING_SCAN_INTERVAL = timedelta(minutes=10)

UPDATE_INTERVAL = DEFAULT_SCAN_INTERVAL
UPDATE_INTERVAL_CHARGING = DEFAULT_CHARGING_SCAN_INTERVAL

# Retry configuration
RETRY_LIMIT = 5
MAX_RETRY_DELAY = 60
RETRY_BACKOFF_FACTOR = 10

# Charging status codes indicating that the vehicle is charging
CHARGING_STATUS_CODES = {1, 3, 10, 12}

# Platforms
PLATFORMS = [
Expand Down
128 changes: 99 additions & 29 deletions custom_components/mg_saic/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
import asyncio
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .api import SAICMGAPIClient
from .const import LOGGER, RETRY_LIMIT, UPDATE_INTERVAL, UPDATE_INTERVAL_CHARGING
from .const import (
LOGGER,
RETRY_LIMIT,
UPDATE_INTERVAL,
UPDATE_INTERVAL_CHARGING,
MAX_RETRY_DELAY,
RETRY_BACKOFF_FACTOR,
CHARGING_STATUS_CODES,
)


class SAICMGDataUpdateCoordinator(DataUpdateCoordinator):
Expand All @@ -12,6 +20,7 @@ def __init__(self, hass, client: SAICMGAPIClient, config_entry):
"""Initialize."""
self.client = client
self.config_entry = config_entry
self.is_charging = False

# Initialize update intervals from config_entry options, falling back to defaults if not set
self.update_interval = timedelta(
Expand All @@ -25,7 +34,8 @@ def __init__(self, hass, client: SAICMGAPIClient, config_entry):
)
)

self.vehicle_type = None
# Use the vehicle type from the config entry
self.vehicle_type = self.config_entry.data.get("vehicle_type")

super().__init__(
hass,
Expand All @@ -41,61 +51,117 @@ async def async_setup(self):

async def _async_update_data(self):
"""Fetch data from the API."""
data = {}
retries = 0
data = {}
while retries < RETRY_LIMIT:
try:
# Fetch vehicle info
vehicle_info = await self.client.get_vehicle_info()
if vehicle_info is None:
LOGGER.error("Vehicle info returned None")
raise UpdateFailed("Vehicle info is None")
data["info"] = vehicle_info
# Fetch charging info to determine charging status
charging_info = await self.client.get_charging_info()
if charging_info is None:
LOGGER.error("Charging info returned None")
raise UpdateFailed("Charging info is None")
data["charging"] = charging_info

# Determine vehicle type
self.vehicle_type = self._determine_vehicle_type(vehicle_info)
# Determine if the vehicle is charging
bms_chrg_sts = getattr(
getattr(charging_info, "chrgMgmtData", None), "bmsChrgSts", None
)
current_is_charging = bms_chrg_sts in CHARGING_STATUS_CODES

# Fetch vehicle status
# Check for charging state change
charging_state_changed = current_is_charging != self.is_charging

# Update charging status
self.is_charging = current_is_charging

# Determine if it's the first run
is_first_run = self.data is None or not self.data

# Fetch Vehicle Info if needed
fetch_vehicle_info = (
charging_state_changed
or self.vehicle_type is None
or is_first_run
or "info" not in self.data
or self.data["info"] is None
)

# Always fetch Vehicle Status
vehicle_status = await self.client.get_vehicle_status()
if vehicle_status is None:
LOGGER.error("Vehicle status returned None")
raise UpdateFailed("Vehicle status is None")

if self._is_generic_response(vehicle_status):
LOGGER.warning(
"Vehicle status is generic, using previous data if available"
)
vehicle_status = self.data.get("status", None)
LOGGER.warning("Vehicle status is generic, retrying...")
raise GenericResponseException("Received generic vehicle status")

# Vehicle status is valid
data["status"] = vehicle_status

# Fetch charging info
charging_info = await self.client.get_charging_info()
if charging_info is None:
LOGGER.error("Charging info returned None")
data["charging"] = charging_info
if fetch_vehicle_info:
# Fetch vehicle info
vehicle_info = await self.client.get_vehicle_info()
if vehicle_info is None:
LOGGER.error("Vehicle info returned None")
raise UpdateFailed("Vehicle info is None")
data["info"] = vehicle_info

else:
data["info"] = self.data.get("info") if self.data else None

LOGGER.debug("Vehicle Type: %s", self.vehicle_type)
LOGGER.debug("Vehicle Info: %s", vehicle_info)
LOGGER.debug("Vehicle Status: %s", vehicle_status)
LOGGER.debug("Vehicle Info: %s", data.get("info"))
LOGGER.debug("Vehicle Status: %s", data.get("status"))
LOGGER.debug("Vehicle Charging Data: %s", charging_info)

# Adjust update interval based on charging status
new_interval = self.update_interval
if charging_info and charging_info.chrgMgmtData.bmsChrgSts == 1:
new_interval = self.update_interval_charging
new_interval = (
self.update_interval_charging
if self.is_charging
else self.update_interval
)

if self.update_interval != new_interval:
self.update_interval = new_interval
self.async_set_update_interval(self.update_interval)
LOGGER.debug("Update interval set to %s", self.update_interval)
# Reschedule the updates with the new interval
if self._unsub_refresh:
self._unsub_refresh()
self._schedule_refresh()

return data

except GenericResponseException as e:
retries += 1
delay = min(retries * RETRY_BACKOFF_FACTOR, MAX_RETRY_DELAY)
LOGGER.warning(
"Received generic response. Retrying (%d/%d) in %s seconds...",
retries,
RETRY_LIMIT,
delay,
)
await asyncio.sleep(delay)
continue
# Retry fetching data
except Exception as e:
LOGGER.error("Error fetching data: %s", e)
retries += 1
await asyncio.sleep(1)
delay = min(retries * RETRY_BACKOFF_FACTOR, MAX_RETRY_DELAY)
LOGGER.info("Retrying in %s seconds...", delay)
await asyncio.sleep(delay)

raise UpdateFailed("Failed to fetch data after retries")
# After retries exhausted, attempt to use previous data
LOGGER.error(
"Failed to fetch valid data after retries. Using previous data if available."
)
if self.data:
LOGGER.info("Using previous data.")
return self.data
else:
raise UpdateFailed(
"Failed to fetch data after retries and no previous data available"
)

def _is_generic_response(self, status):
"""Check if the response is generic."""
Expand Down Expand Up @@ -159,3 +225,7 @@ def _determine_vehicle_type(self, vehicle_info):
return "ICE"

return "ICE"


class GenericResponseException(Exception):
"""Exception raised when a generic response is received."""
4 changes: 2 additions & 2 deletions custom_components/mg_saic/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"requirements": [
"requests",
"pycryptodome",
"saic-ismart-client-ng==0.2.3"
"saic-ismart-client-ng==0.5.2"
],
"version": "0.4.1"
"version": "0.4.2"
}
41 changes: 25 additions & 16 deletions custom_components/mg_saic/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(self, coordinator, client, vin_info, vin):
self._client = client
self._vin = vin
self._vin_info = vin_info
self._last_valid_value = None

self._attr_name = f"{vin_info.brandName} {vin_info.modelName} Target SOC"
self._attr_unique_id = f"{vin}_target_soc"
Expand Down Expand Up @@ -70,27 +71,35 @@ def native_value(self):
6: 90,
7: 100,
}
return soc_mapping.get(soc_cmd)
return None
soc_value = soc_mapping.get(soc_cmd)
if soc_value is not None:
self._last_valid_value = soc_value
return soc_value
# Return the last valid value if current data is invalid
return self._last_valid_value

@property
def icon(self):
"""Return the icon based on the current SOC value."""
if self.native_value >= 100:
return "mdi:battery-charging-100"
elif self.native_value >= 90:
return "mdi:battery-charging-90"
elif self.native_value >= 80:
return "mdi:battery-charging-80"
elif self.native_value >= 70:
return "mdi:battery-charging-70"
elif self.native_value >= 60:
return "mdi:battery-charging-60"
elif self.native_value >= 50:
return "mdi:battery-charging-50"
elif self.native_value >= 40:
return "mdi:battery-charging-40"
if self.native_value is not None:
if self.native_value >= 100:
return "mdi:battery-charging-100"
elif self.native_value >= 90:
return "mdi:battery-charging-90"
elif self.native_value >= 80:
return "mdi:battery-charging-80"
elif self.native_value >= 70:
return "mdi:battery-charging-70"
elif self.native_value >= 60:
return "mdi:battery-charging-60"
elif self.native_value >= 50:
return "mdi:battery-charging-50"
elif self.native_value >= 40:
return "mdi:battery-charging-40"
else:
return "mdi:battery-charging-outline"
else:
# Return a default icon when native_value is None
return "mdi:battery-charging-outline"

async def async_set_native_value(self, value: float) -> None:
Expand Down
Loading

0 comments on commit 62a1521

Please sign in to comment.