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 switch platform to Teslemetry #117482

Merged
merged 10 commits into from
May 23, 2024
Merged
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
1 change: 1 addition & 0 deletions homeassistant/components/teslemetry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
Platform.CLIMATE,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]


Expand Down
35 changes: 35 additions & 0 deletions homeassistant/components/teslemetry/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,41 @@
"wall_connector_state": {
"default": "mdi:ev-station"
}
},
"switch": {
"charge_state_user_charge_enable_request": {
"default": "mdi:ev-station"
},
"climate_state_auto_seat_climate_left": {
"default": "mdi:car-seat-heater",
"state": {
"off": "mdi:car-seat"
}
},
"climate_state_auto_seat_climate_right": {
"default": "mdi:car-seat-heater",
"state": {
"off": "mdi:car-seat"
}
},
"climate_state_auto_steering_wheel_heat": {
"default": "mdi:steering"
},
"climate_state_defrost_mode": {
"default": "mdi:snowflake-melt"
},
"components_disallow_charge_from_grid_with_solar_installed": {
"state": {
"false": "mdi:transmission-tower",
"true": "mdi:solar-power"
}
},
"vehicle_state_sentry_mode": {
"default": "mdi:shield-car"
},
"vehicle_state_valet_mode": {
"default": "mdi:speedometer-slow"
}
}
}
}
29 changes: 29 additions & 0 deletions homeassistant/components/teslemetry/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,35 @@
"wall_connector_state": {
"name": "State code"
}
},
"switch": {
"charge_state_user_charge_enable_request": {
"name": "Charge"
},
"climate_state_auto_seat_climate_left": {
"name": "Auto seat climate left"
},
"climate_state_auto_seat_climate_right": {
"name": "Auto seat climate right"
},
"climate_state_auto_steering_wheel_heat": {
"name": "Auto steering wheel heater"
},
"climate_state_defrost_mode": {
"name": "Defrost"
},
"components_disallow_charge_from_grid_with_solar_installed": {
"name": "Allow charging from grid"
},
"user_settings_storm_mode_enabled": {
"name": "Storm watch"
},
"vehicle_state_sentry_mode": {
"name": "Sentry mode"
},
"vehicle_state_valet_mode": {
"name": "Valet mode"
}
}
}
}
257 changes: 257 additions & 0 deletions homeassistant/components/teslemetry/switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
"""Switch platform for Teslemetry integration."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from itertools import chain
from typing import Any

from tesla_fleet_api.const import Scope, Seat

from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .entity import TeslemetryEnergyInfoEntity, TeslemetryVehicleEntity
from .models import TeslemetryEnergyData, TeslemetryVehicleData


@dataclass(frozen=True, kw_only=True)
class TeslemetrySwitchEntityDescription(SwitchEntityDescription):
"""Describes Teslemetry Switch entity."""

on_func: Callable
off_func: Callable
scopes: list[Scope]


VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = (
TeslemetrySwitchEntityDescription(
key="vehicle_state_sentry_mode",
on_func=lambda api: api.set_sentry_mode(on=True),
off_func=lambda api: api.set_sentry_mode(on=False),
scopes=[Scope.VEHICLE_CMDS],
),
TeslemetrySwitchEntityDescription(
key="climate_state_auto_seat_climate_left",
on_func=lambda api: api.remote_auto_seat_climate_request(Seat.FRONT_LEFT, True),
off_func=lambda api: api.remote_auto_seat_climate_request(
Seat.FRONT_LEFT, False
),
scopes=[Scope.VEHICLE_CMDS],
),
TeslemetrySwitchEntityDescription(
key="climate_state_auto_seat_climate_right",
on_func=lambda api: api.remote_auto_seat_climate_request(
Seat.FRONT_RIGHT, True
),
off_func=lambda api: api.remote_auto_seat_climate_request(
Seat.FRONT_RIGHT, False
),
scopes=[Scope.VEHICLE_CMDS],
),
TeslemetrySwitchEntityDescription(
key="climate_state_auto_steering_wheel_heat",
on_func=lambda api: api.remote_auto_steering_wheel_heat_climate_request(
on=True
),
off_func=lambda api: api.remote_auto_steering_wheel_heat_climate_request(
on=False
),
scopes=[Scope.VEHICLE_CMDS],
),
TeslemetrySwitchEntityDescription(
key="climate_state_defrost_mode",
on_func=lambda api: api.set_preconditioning_max(on=True, manual_override=False),
off_func=lambda api: api.set_preconditioning_max(
on=False, manual_override=False
),
scopes=[Scope.VEHICLE_CMDS],
),
)

VEHICLE_CHARGE_DESCRIPTION = TeslemetrySwitchEntityDescription(
key="charge_state_user_charge_enable_request",
on_func=lambda api: api.charge_start(),
off_func=lambda api: api.charge_stop(),
scopes=[Scope.VEHICLE_CMDS, Scope.VEHICLE_CHARGING_CMDS],
)


async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Teslemetry Switch platform from a config entry."""

async_add_entities(
chain(
(
TeslemetryVehicleSwitchEntity(
vehicle, description, entry.runtime_data.scopes
)
for vehicle in entry.runtime_data.vehicles
for description in VEHICLE_DESCRIPTIONS
),
(
TeslemetryChargeSwitchEntity(
vehicle, VEHICLE_CHARGE_DESCRIPTION, entry.runtime_data.scopes
)
for vehicle in entry.runtime_data.vehicles
),
(
TeslemetryChargeFromGridSwitchEntity(
energysite,
entry.runtime_data.scopes,
)
for energysite in entry.runtime_data.energysites
if energysite.info_coordinator.data.get("components_battery")
and energysite.info_coordinator.data.get("components_solar")
),
(
TeslemetryStormModeSwitchEntity(energysite, entry.runtime_data.scopes)
for energysite in entry.runtime_data.energysites
if energysite.info_coordinator.data.get("components_storm_mode_capable")
),
)
)


class TeslemetrySwitchEntity(SwitchEntity):
"""Base class for all Teslemetry switch entities."""

_attr_device_class = SwitchDeviceClass.SWITCH
entity_description: TeslemetrySwitchEntityDescription


class TeslemetryVehicleSwitchEntity(TeslemetryVehicleEntity, TeslemetrySwitchEntity):
"""Base class for Teslemetry vehicle switch entities."""

def __init__(
self,
data: TeslemetryVehicleData,
description: TeslemetrySwitchEntityDescription,
scopes: list[Scope],
) -> None:
"""Initialize the Switch."""
super().__init__(data, description.key)
self.entity_description = description
self.scoped = any(scope in scopes for scope in description.scopes)

def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
if self._value is None:
self._attr_is_on = None
else:
self._attr_is_on = bool(self._value)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Switch."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.entity_description.on_func(self.api))
self._attr_is_on = True
self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Switch."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.entity_description.off_func(self.api))
self._attr_is_on = False
self.async_write_ha_state()


class TeslemetryChargeSwitchEntity(TeslemetryVehicleSwitchEntity):
"""Entity class for Teslemetry charge switch."""

def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
if self._value is None:
self._attr_is_on = self.get("charge_state_charge_enable_request")
else:
self._attr_is_on = self._value


class TeslemetryChargeFromGridSwitchEntity(
TeslemetryEnergyInfoEntity, TeslemetrySwitchEntity
):
"""Entity class for Charge From Grid switch."""

def __init__(
self,
data: TeslemetryEnergyData,
scopes: list[Scope],
) -> None:
"""Initialize the Switch."""
self.scoped = Scope.ENERGY_CMDS in scopes
super().__init__(
data, "components_disallow_charge_from_grid_with_solar_installed"
)

def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
# When disallow_charge_from_grid_with_solar_installed is missing, its Off.
# But this sensor is flipped to match how the Tesla app works.
self._attr_is_on = not self.get(self.key, False)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Switch."""
self.raise_for_scope()
await self.handle_command(
self.api.grid_import_export(
disallow_charge_from_grid_with_solar_installed=False
)
)
self._attr_is_on = True
self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Switch."""
self.raise_for_scope()
await self.handle_command(
self.api.grid_import_export(
disallow_charge_from_grid_with_solar_installed=True
)
)
self._attr_is_on = False
self.async_write_ha_state()


class TeslemetryStormModeSwitchEntity(
TeslemetryEnergyInfoEntity, TeslemetrySwitchEntity
):
"""Entity class for Storm Mode switch."""

def __init__(
self,
data: TeslemetryEnergyData,
scopes: list[Scope],
) -> None:
"""Initialize the Switch."""
super().__init__(data, "user_settings_storm_mode_enabled")
self.scoped = Scope.ENERGY_CMDS in scopes

def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = self._value is not None
self._attr_is_on = bool(self._value)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Switch."""
self.raise_for_scope()
await self.handle_command(self.api.storm_mode(enabled=True))
self._attr_is_on = True
self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Switch."""
self.raise_for_scope()
await self.handle_command(self.api.storm_mode(enabled=False))
self._attr_is_on = False
self.async_write_ha_state()
2 changes: 1 addition & 1 deletion tests/components/teslemetry/fixtures/vehicle_data_alt.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"timestamp": null,
"trip_charging": false,
"usable_battery_level": 77,
"user_charge_enable_request": null
"user_charge_enable_request": true
},
"climate_state": {
"allow_cabin_overheat_protection": true,
Expand Down
Loading