Skip to content

Commit

Permalink
feat: add support for Heated Steering Wheel and Seats (#188)
Browse files Browse the repository at this point in the history
  • Loading branch information
Megabytemb authored Apr 24, 2022
1 parent dc1ba93 commit c052539
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 0 deletions.
50 changes: 50 additions & 0 deletions custom_components/tesla_custom/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from . import DOMAIN as TESLA_DOMAIN
from .tesla_device import TeslaDevice
from .helpers import get_device

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -97,6 +98,7 @@ async def async_set_hvac_mode(self, hvac_mode):
await self.tesla_device.set_status(False)
elif hvac_mode == HVAC_MODE_HEAT_COOL:
await self.tesla_device.set_status(True)
await self.update_climate_related_devices()
self.async_write_ha_state()

@TeslaDevice.Decorators.check_for_reauth
Expand Down Expand Up @@ -124,3 +126,51 @@ def preset_modes(self) -> list[str] | None:
Requires SUPPORT_PRESET_MODE.
"""
return self.tesla_device.preset_modes

async def update_climate_related_devices(self):
"""Reset the Manual Update time on climate related devices.
This way, their states are correctly reflected if they are dependant on the Climate state.
"""

# This is really gross, and i kinda hate it.
# but its the only way i could figure out how to force an update on the underlying device
# thats in the teslajsonpy library.
# This could be fixed by doing a pr in the underlying library,
# but is ok for now.

# This works by reseting the last update time in the underlying device.
# this does not cause an api call, but instead enabled the undering device
# to read from the shared climate data cache in the teslajsonpy library.

# First, we need to force the controller to update, as the refresh functions asume it
# has been uddated.
# We have to manually update the controller becuase changing the HVAC state only updates its state in Home assistant,
# and not the underlying cache in cliamte_parms. This does mean we talk to Tesla, but we only do so Once.

await self.tesla_device._controller.update(
self.tesla_device._id, wake_if_asleep=False, force=True
)

climate_devices = [
["switch", "heated steering switch"],
["select", "heated seat left"],
["select", "heated seat right"],
["select", "heated seat rear_left"],
["select", "heated seat rear_center"],
["select", "heated seat rear_right"],
]

for c_device in climate_devices:
_LOGGER.debug("Refreshing Device: %s.%s", c_device[0], c_device[1])

device = await get_device(
self.hass, self.config_entry_id, c_device[0], c_device[1]
)
if device is not None:
class_name = device.__class__.__name__
attr_str = f"_{class_name}__manual_update_time"
setattr(device, attr_str, 0)

# Does not cause an API call.
device.refresh()
2 changes: 2 additions & 0 deletions custom_components/tesla_custom/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"device_tracker",
"switch",
"button",
"select",
]

ICONS = {
Expand All @@ -37,6 +38,7 @@
"flash lights": "mdi:car-light-high",
"trigger homelink": "mdi:garage",
"solar panel": "mdi:solar-panel",
"heated steering wheel": "mdi:steering",
}
AUTH_CALLBACK_PATH = "/auth/tesla/callback"
AUTH_CALLBACK_NAME = "auth:tesla:callback"
Expand Down
73 changes: 73 additions & 0 deletions custom_components/tesla_custom/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Helpers module.
A collection of functions which may be used accross entities
"""
from .const import DOMAIN as TESLA_DOMAIN

import asyncio
import async_timeout
import logging

from homeassistant.core import HomeAssistant

_LOGGER = logging.getLogger(__name__)


async def get_device(
hass: HomeAssistant,
config_entry_id: str,
device_category: str,
device_type: str,
):
"""Get a tesla Device for a Config Entry ID."""

entry_data = hass.data[TESLA_DOMAIN][config_entry_id]
devices = entry_data["devices"].get(device_category, [])

for device in devices:
if device.type == device_type:
return device

return None


async def wait_for_climate(
hass: HomeAssistant, config_entry_id: str, timeout: int = 30
):
"""Wait for HVac.
Optional Timeout. defaults to 30 seconds
"""
climate_device = await get_device(
hass, config_entry_id, "climate", "HVAC (climate) system"
)

if climate_device is None:
return None

async with async_timeout.timeout(timeout):
while True:
hvac_mode = climate_device.is_hvac_enabled()

if hvac_mode is True:
_LOGGER.debug("HVAC Enabled")
return True
else:
_LOGGER.info("Enabing Climate to activate Heated Steering Wheel")

# The below is a blocking funtion (it waits for a reponse from the API).
# So it could eat into our timeout, and this is fine.
# We'll try to turn the set the status, and check again
try:
await climate_device.set_status(True)
continue
except:
# If we get an error, we'll just loop around and try again
pass

# Wait two second between API calls, in case we get an error like car unavail,
# or any other random thing tesla throws at us
await asyncio.sleep(2)

# we'll return false if the timeout is reached.
return False
55 changes: 55 additions & 0 deletions custom_components/tesla_custom/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Support for Tesla selects."""
import logging

from homeassistant.components.select import SelectEntity

from . import DOMAIN as TESLA_DOMAIN
from .tesla_device import TeslaDevice
from .helpers import wait_for_climate

_LOGGER = logging.getLogger(__name__)

OPTIONS = [
"Off",
"Low",
"Medium",
"High",
]


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Tesla selects by config_entry."""
coordinator = hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"]
entities = []
for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["select"]:
if device.type.startswith("heated seat "):
entities.append(HeatedSeatSelect(device, coordinator))
async_add_entities(entities, True)


class HeatedSeatSelect(TeslaDevice, SelectEntity):
"""Representation of a Tesla Heated Seat Select."""

@TeslaDevice.Decorators.check_for_reauth
async def async_select_option(self, option: str, **kwargs):
"""Change the selected option."""
level: int = OPTIONS.index(option)

await wait_for_climate(self.hass, self.config_entry_id)
_LOGGER.debug("Setting %s to %s", self.name, level)
await self.tesla_device.set_seat_heat_level(level)
self.async_write_ha_state()

@property
def current_option(self):
"""Return the selected entity option to represent the entity state."""
current_value = self.tesla_device.get_seat_heat_level()

if current_value is None:
return OPTIONS[0]
return OPTIONS[current_value]

@property
def options(self):
"""Return a set of selectable options."""
return OPTIONS
32 changes: 32 additions & 0 deletions custom_components/tesla_custom/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from . import DOMAIN as TESLA_DOMAIN
from .tesla_device import TeslaDevice
from .helpers import wait_for_climate

_LOGGER = logging.getLogger(__name__)

Expand All @@ -22,9 +23,40 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(RangeSwitch(device, coordinator))
elif device.type == "sentry mode switch":
entities.append(SentryModeSwitch(device, coordinator))
elif device.type == "heated steering switch":
entities.append(HeatedSteeringWheelSwitch(device, coordinator))
async_add_entities(entities, True)


class HeatedSteeringWheelSwitch(TeslaDevice, SwitchEntity):
"""Representation of a Tesla Heated Steering Wheel switch."""

@TeslaDevice.Decorators.check_for_reauth
async def async_turn_on(self, **kwargs):
"""Send the on command."""
_LOGGER.debug("Turn on Heating Steering Wheel: %s", self.name)
await wait_for_climate(self.hass, self.config_entry_id)
await self.tesla_device.set_steering_wheel_heat(True)
self.async_write_ha_state()

@TeslaDevice.Decorators.check_for_reauth
async def async_turn_off(self, **kwargs):
"""Send the off command."""
_LOGGER.debug("Turn off Heating Steering Wheel: %s", self.name)
await self.tesla_device.set_steering_wheel_heat(False)
self.async_write_ha_state()

@property
def is_on(self):
"""Get whether the switch is in on state."""
return self.tesla_device.get_steering_wheel_heat()

@property
def icon(self):
"""Return the icon of the sensor."""
return ICONS.get("heated steering wheel")


class ChargerSwitch(TeslaDevice, SwitchEntity):
"""Representation of a Tesla charger switch."""

Expand Down

0 comments on commit c052539

Please sign in to comment.