Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Heated Steering Wheel and Seats #188

Merged
merged 16 commits into from
Apr 24, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
37 changes: 37 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,38 @@ 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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By adding a public method to allow resetting the manual update time?
Perhaps it's worth filing an issue on the teslajsonpy repo, so this doesn't get forgotten.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's open an issue. Basically what you're saying we need is a way to force a device to reread its info from cache.

I'm ok with the current approach for a 1.0 type release, but it makes sense to resolve it more cleanly in the future.

# 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.

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:
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)
await device.async_update(force=True, wake_if_asleep=False)
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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How often on your system does this hit the Tesla endpoint? The issue I'm concerned about is that each api call has a wake call which includes 5 separate retries. Since this loop in theory can hit 15 times and if there's a wakeup that quickly goes to 75 api hits. Are we slamming the endpoint unnecessarily?

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
54 changes: 54 additions & 0 deletions custom_components/tesla_custom/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""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."""

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