Skip to content

Commit

Permalink
Merge pull request #15 from ad-ha/ad-ha-0.4.1
Browse files Browse the repository at this point in the history
0.4.1
  • Loading branch information
ad-ha authored Oct 29, 2024
2 parents 39f4848 + 591845b commit bf4aa41
Show file tree
Hide file tree
Showing 13 changed files with 825 additions and 6 deletions.
13 changes: 13 additions & 0 deletions custom_components/mg_saic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):

try:
await client.login()

# Fetch vehicle info to get the VIN
vehicles = await client.get_vehicle_info()
if vehicles:
vin = vehicles[0].vin
else:
LOGGER.error("No vehicles found for this account.")
return False

hass.data[DOMAIN][entry.entry_id] = client

coordinator = SAICMGDataUpdateCoordinator(hass, client, entry)
await coordinator.async_setup()

hass.data[DOMAIN][f"{entry.entry_id}_coordinator"] = coordinator

# Store coordinator by VIN for data refresh after service calls
hass.data[DOMAIN].setdefault("coordinators_by_vin", {})
hass.data[DOMAIN]["coordinators_by_vin"][vin] = coordinator

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

# Register Services
Expand Down
44 changes: 43 additions & 1 deletion custom_components/mg_saic/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ async def login(self):
"Logging in with region-based endpoint for region: %s", self.region
)

# Use asyncio.to_thread to avoid blocking the event loop with a blocking call
self.saic_api = await asyncio.to_thread(SaicApi, config)

try:
Expand Down Expand Up @@ -135,6 +134,49 @@ async def stop_ac(self, vin):
except Exception as e:
LOGGER.error("Error stopping AC: %s", e)

async def start_climate(self, vin, temperature, fan_speed):
"""Start the vehicle AC with temperature and fan speed settings."""
await self._ensure_initialized()
try:
# Map temperature in Celsius to temperature_idx expected by the API
temperature_idx = self._map_temperature_to_idx(temperature)
await self.saic_api.control_climate(
vin, fan_speed=fan_speed, ac_on=True, temperature_idx=temperature_idx
)
LOGGER.info(
"AC started with temperature %s°C and fan speed %s for VIN: %s",
temperature,
fan_speed,
vin,
)
except Exception as e:
LOGGER.error("Error starting AC with settings for VIN %s: %s", vin, e)
raise

def _map_temperature_to_idx(self, temperature):
"""Map temperature in Celsius to temperature_idx expected by the API."""
temperature_to_idx = {
16: 0,
17: 1,
18: 2,
19: 3,
20: 4,
21: 5,
22: 6,
23: 7,
24: 8,
25: 9,
26: 10,
27: 11,
28: 12,
29: 13,
30: 14,
}
idx = temperature_to_idx.get(int(temperature))
if idx is None:
raise ValueError("Invalid temperature value. Must be between 16 and 30°C.")
return idx

# Locks control actions
async def lock_vehicle(self, vin):
"""Lock the vehicle."""
Expand Down
144 changes: 144 additions & 0 deletions custom_components/mg_saic/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from homeassistant.components.button import ButtonEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, LOGGER
import asyncio


async def async_setup_entry(hass, entry, async_add_entities):
"""Set up MG SAIC buttons."""
coordinator = hass.data[DOMAIN][f"{entry.entry_id}_coordinator"]
client = hass.data[DOMAIN][entry.entry_id]
vin_info = coordinator.data["info"][0]
vin = vin_info.vin

buttons = [
SAICMGOpenTailgateButton(coordinator, client, vin_info, vin),
SAICMGTriggerAlarmButton(coordinator, client, vin_info, vin),
SAICMGStartFrontDefrostButton(coordinator, client, vin_info, vin),
SAICMGControlRearWindowHeatButton(coordinator, client, vin_info, vin),
]

async_add_entities(buttons)


class SAICMGButton(CoordinatorEntity, ButtonEntity):
"""Base class for MG SAIC buttons."""

def __init__(self, coordinator, client, vin_info, vin, name, icon):
"""Initialize the button."""
super().__init__(coordinator)
self._client = client
self._vin = vin
self._vin_info = vin_info
self._attr_name = f"{vin_info.brandName} {vin_info.modelName} {name}"
self._attr_unique_id = f"{vin}_{name.replace(' ', '_').lower()}_button"
self._attr_icon = icon

@property
def device_info(self):
"""Return device info."""
return {
"identifiers": {(DOMAIN, self._vin)},
"name": f"{self._vin_info.brandName} {self._vin_info.modelName}",
"manufacturer": self._vin_info.brandName,
"model": self._vin_info.modelName,
"serial_number": self._vin,
}

async def schedule_data_refresh(self):
"""Schedule a data refresh for the coordinator associated with the VIN."""
coordinators_by_vin = self.hass.data[DOMAIN].get("coordinators_by_vin", {})
coordinator = coordinators_by_vin.get(self._vin)
if coordinator:

async def delayed_refresh():
await asyncio.sleep(15) # Wait for 15 seconds
await coordinator.async_request_refresh()

self.hass.async_create_task(delayed_refresh())
else:
LOGGER.warning("Coordinator not found for VIN %s", self._vin)


class SAICMGOpenTailgateButton(SAICMGButton):
"""Button to open the tailgate."""

def __init__(self, coordinator, client, vin_info, vin):
super().__init__(
coordinator, client, vin_info, vin, "Open Tailgate", "mdi:car-back"
)

async def async_press(self):
"""Handle the button press."""
try:
await self._client.open_tailgate(self._vin)
LOGGER.info("Tailgate opened for VIN: %s", self._vin)
await self.schedule_data_refresh()
except Exception as e:
LOGGER.error("Error opening tailgate for VIN %s: %s", self._vin, e)


class SAICMGTriggerAlarmButton(SAICMGButton):
"""Button to trigger the vehicle alarm."""

def __init__(self, coordinator, client, vin_info, vin):
super().__init__(
coordinator, client, vin_info, vin, "Trigger Alarm", "mdi:alarm-light"
)

async def async_press(self):
"""Handle the button press."""
try:
await self._client.trigger_alarm(self._vin)
LOGGER.info("Alarm triggered for VIN: %s", self._vin)
await self.schedule_data_refresh()
except Exception as e:
LOGGER.error("Error triggering alarm for VIN %s: %s", self._vin, e)


class SAICMGStartFrontDefrostButton(SAICMGButton):
"""Button to start front defrost."""

def __init__(self, coordinator, client, vin_info, vin):
super().__init__(
coordinator,
client,
vin_info,
vin,
"Start Front Defrost",
"mdi:car-defrost-front",
)

async def async_press(self):
"""Handle the button press."""
try:
await self._client.start_front_defrost(self._vin)
LOGGER.info("Front defrost started for VIN: %s", self._vin)
await self.schedule_data_refresh()
except Exception as e:
LOGGER.error("Error starting front defrost for VIN %s: %s", self._vin, e)


class SAICMGControlRearWindowHeatButton(SAICMGButton):
"""Button to activate rear window heat."""

def __init__(self, coordinator, client, vin_info, vin):
super().__init__(
coordinator,
client,
vin_info,
vin,
"Activate Rear Window Heat",
"mdi:car-defrost-rear",
)

async def async_press(self):
"""Handle the button press."""
try:
await self._client.control_rear_window_heat(self._vin, True)
LOGGER.info("Rear window heat activated for VIN: %s", self._vin)
await self.schedule_data_refresh()
except Exception as e:
LOGGER.error(
"Error activating rear window heat for VIN %s: %s", self._vin, e
)
153 changes: 153 additions & 0 deletions custom_components/mg_saic/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.components.climate.const import (
FAN_LOW,
FAN_MEDIUM,
FAN_HIGH,
)
from homeassistant.const import (
UnitOfTemperature,
ATTR_TEMPERATURE,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, LOGGER


async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the MG SAIC climate entity."""
coordinator = hass.data[DOMAIN][f"{entry.entry_id}_coordinator"]
client = hass.data[DOMAIN][entry.entry_id]
vin_info = coordinator.data["info"][0]
vin = vin_info.vin

climate_entity = SAICMGClimateEntity(coordinator, client, vin_info, vin)
async_add_entities([climate_entity])


class SAICMGClimateEntity(CoordinatorEntity, ClimateEntity):
"""Representation of the vehicle's climate control."""

def __init__(self, coordinator, client, vin_info, vin):
"""Initialize the climate entity."""
super().__init__(coordinator)
self._client = client
self._vin = vin
self._vin_info = vin_info

self._attr_name = f"{vin_info.brandName} {vin_info.modelName} Climate"
self._attr_unique_id = f"{vin}_climate"
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL]
self._attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH]

# Initialize with default values
self._attr_current_temperature = None
self._attr_target_temperature = 22.0 # Default target temperature
self._attr_fan_mode = FAN_MEDIUM
self._attr_hvac_mode = HVACMode.OFF

@property
def device_info(self):
"""Return device info."""
return {
"identifiers": {(DOMAIN, self._vin)},
"name": f"{self._vin_info.brandName} {self._vin_info.modelName}",
"manufacturer": self._vin_info.brandName,
"model": self._vin_info.modelName,
"serial_number": self._vin,
}

@property
def current_temperature(self):
"""Return the current interior temperature."""
status = self.coordinator.data.get("status")
if status:
interior_temp = getattr(
status.basicVehicleStatus, "interiorTemperature", None
)
if interior_temp is not None and interior_temp != -128:
return interior_temp # Adjust if necessary
return None

@property
def hvac_mode(self):
"""Return the current HVAC mode."""
status = self.coordinator.data.get("status")
if status:
ac_status = getattr(status.basicVehicleStatus, "remoteClimateStatus", None)
if ac_status == 1:
return HVACMode.COOL
return HVACMode.OFF

async def async_set_hvac_mode(self, hvac_mode):
"""Set the HVAC mode."""
if hvac_mode == HVACMode.OFF:
await self._client.stop_ac(self._vin)
self._attr_hvac_mode = HVACMode.OFF
elif hvac_mode == HVACMode.COOL:
await self._client.start_climate(
self._vin, self._attr_target_temperature, self._fan_speed_to_int()
)
self._attr_hvac_mode = HVACMode.COOL
else:
LOGGER.warning("Unsupported HVAC mode: %s", hvac_mode)
# Schedule data refresh
await self.coordinator.async_request_refresh()

async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is not None:
self._attr_target_temperature = temperature
if self.hvac_mode != HVACMode.OFF:
await self._client.start_climate(
self._vin, temperature, self._fan_speed_to_int()
)
# Schedule data refresh
await self.coordinator.async_request_refresh()

@property
def fan_mode(self):
"""Return the current fan mode."""
return self._attr_fan_mode

async def async_set_fan_mode(self, fan_mode):
"""Set the fan mode."""
if fan_mode in self._attr_fan_modes:
self._attr_fan_mode = fan_mode
if self.hvac_mode != HVACMode.OFF:
await self._client.start_climate(
self._vin, self._attr_target_temperature, self._fan_speed_to_int()
)
# Schedule data refresh
await self.coordinator.async_request_refresh()
else:
LOGGER.warning("Unsupported fan mode: %s", fan_mode)

def _fan_speed_to_int(self):
"""Convert fan mode to integer value expected by the API."""
mapping = {
FAN_LOW: 1,
FAN_MEDIUM: 3,
FAN_HIGH: 5,
}
fan_speed = mapping.get(self._attr_fan_mode)
if fan_speed is None:
raise ValueError("Invalid fan mode.")
return fan_speed

@property
def min_temp(self):
"""Return the minimum temperature."""
return 16.0

@property
def max_temp(self):
"""Return the maximum temperature."""
return 30.0
11 changes: 10 additions & 1 deletion custom_components/mg_saic/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@
RETRY_LIMIT = 3

# Platforms
PLATFORMS = ["sensor", "binary_sensor", "device_tracker"]
PLATFORMS = [
"sensor",
"binary_sensor",
"device_tracker",
"button",
"climate",
"number",
"switch",
"lock",
]
Loading

0 comments on commit bf4aa41

Please sign in to comment.