Skip to content

Commit

Permalink
Merge pull request #754 from alandtse/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
alandtse authored Oct 29, 2023
2 parents 93f3b02 + 0ae5bea commit 620aacb
Show file tree
Hide file tree
Showing 18 changed files with 407 additions and 366 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ repos:
- hooks:
- id: black
repo: https://github.com/psf/black
rev: 23.9.1
rev: 23.10.1
- repo: https://github.com/pre-commit/mirrors-prettier
hooks:
- id: prettier
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ To use the component, you will need an application to generate a Tesla refresh t
## Installation

1. Use [HACS](https://hacs.xyz/docs/setup/download), in `HACS > Integrations > Explore & Add Repositories` search for "Tesla". After adding this `https://github.com/alandtse/tesla` as a custom repository. Skip to 7.
2. If no HACS, use the tool of choice to open the directory (folder) for your HA configuration (where you find `configuration.yaml`).
2. If you do not have HACS, use the tool of choice to open the directory (folder) for your HA configuration (where you find `configuration.yaml`).
3. If you do not have a `custom_components` directory (folder) there, you need to create it.
4. In the `custom_components` directory (folder) create a new folder called `tesla_custom`.
5. Download _all_ the files from the `custom_components/tesla_custom/` directory (folder) in this repository.
Expand Down Expand Up @@ -72,7 +72,7 @@ Tesla options are set via **Configuration** -> **Integrations** -> **Tesla** ->
- Seconds between polling - referred to below as the `polling_interval`.
- Wake cars on start - Whether to wake sleeping cars on Home Assistant startup. This allows a user to choose whether cars should continue to sleep (and not update information) or to wake up the cars potentially interrupting long term hibernation and increasing vampire drain.
- Polling policy - When do we actively poll the car to get updates, and when do we try to allow the car to sleep. See [the Wiki](https://github.com/alandtse/tesla/wiki/Polling-policy) for more information.
- Sync Data from TeslaMate via MQTT - Enable syncing of Data from an TeslaMate instance via MQTT, esentially enabling the Streaming API for updates. This requies MQTT to be configured in Home Assistant.
- Sync Data from TeslaMate via MQTT - Enable syncing of Data from an TeslaMate instance via MQTT, essentially enabling the Streaming API for updates. This requires MQTT to be configured in Home Assistant.

## Potential Battery impacts

Expand Down
2 changes: 1 addition & 1 deletion custom_components/tesla_custom/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/alandtse/tesla/issues",
"loggers": ["teslajsonpy"],
"requirements": ["teslajsonpy==3.9.5"],
"requirements": ["teslajsonpy==3.9.6"],
"version": "3.18.0"
}
12 changes: 10 additions & 2 deletions custom_components/tesla_custom/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,20 @@ def native_value(self) -> int:
@property
def native_min_value(self) -> int:
"""Return min charge limit."""
return self._car.charge_limit_soc_min
return (
self._car.charge_limit_soc_min
if self._car.charge_limit_soc_min is not None
else 0
)

@property
def native_max_value(self) -> int:
"""Return max charge limit."""
return self._car.charge_limit_soc_max
return (
self._car.charge_limit_soc_max
if self._car.charge_limit_soc_max is not None
else 100
)

@property
def native_unit_of_measurement(self) -> str:
Expand Down
18 changes: 13 additions & 5 deletions custom_components/tesla_custom/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,25 +49,29 @@ def async_setup_services(hass) -> None:
async def async_call_tesla_service(service_call) -> None:
"""Call correct Tesla service."""
service = service_call.service
response = None

if service == SERVICE_API:
await api(service_call)
response = await api(service_call)
elif service == SERVICE_SCAN_INTERVAL:
response = await set_update_interval(service_call)

if service == SERVICE_SCAN_INTERVAL:
await set_update_interval(service_call)
return response

hass.services.async_register(
DOMAIN,
SERVICE_API,
async_call_tesla_service,
schema=API_SCHEMA,
supports_response=True,
)

hass.services.async_register(
DOMAIN,
SERVICE_SCAN_INTERVAL,
async_call_tesla_service,
schema=SCAN_INTERVAL_SCHEMA,
supports_response=True,
)

async def api(call):
Expand Down Expand Up @@ -108,7 +112,8 @@ async def api(call):
parameters,
)
path_vars = parameters.pop(ATTR_PATH_VARS)
return await controller.api(name=command, path_vars=path_vars, **parameters)
response = await controller.api(name=command, path_vars=path_vars, **parameters)
return response

async def set_update_interval(call):
"""Handle api service request.
Expand Down Expand Up @@ -158,7 +163,10 @@ async def set_update_interval(call):
vin,
)
controller.set_update_interval_vin(vin=vin, value=update_interval)
return True
return {
"result": True,
"message": f"Update interval set to {update_interval} for VIN {vin}",
}


@callback
Expand Down
130 changes: 79 additions & 51 deletions custom_components/tesla_custom/teslamate.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@ def cast_km_to_miles(km_to_convert: float) -> float:
return miles


def cast_plugged_in(val: str) -> str:
"""Convert boolean string for plugged_in value."""
return "Connected" if cast_bool(val) else "Disconnected"


def cast_bool(val: str) -> bool:
"""Convert bool string to actual bool."""
return val.lower() in ["true", "True"]
Expand All @@ -74,6 +69,45 @@ def cast_speed(speed: int) -> int:
return int(speed_miles)


def cast_plugged_in(val: str, car: TeslaCar) -> str:
"""Casts new car plugged_in.
When receiving a new value here, we also need to check the
car state to see if it is charging or not before returning
a value to be set as 'charging_state'
"""
plugged_in = cast_bool(val)

logger.debug(
"Casting plugged_in. Current state: '%s', plugged_in: '%s'",
car.state,
plugged_in,
)

if plugged_in:
return "Charging" if car.state == "charging" else "Stopped"

return "Disconnected"


def cast_car_state(state: str, car: TeslaCar) -> (str, str):
"""Casts new car state to both state and charging_state.
Since Teslamate doesn't have a direct 'charging_state' we need both
'state' and 'plugged_in' to determine the charging state. This
method uses both to determine the new state and charging_state.
"""
logger.debug(
"Casting car state. Current charging_state '%s', new state: '%s'",
car.charging_state,
state,
)

if state == "charging":
return (state, "Charging")
return (state, "Stopped" if car.charging_state == "Stopped" else "Disconnected")


MAP_DRIVE_STATE = {
"latitude": ("latitude", float),
"longitude": ("longitude", float),
Expand Down Expand Up @@ -112,7 +146,6 @@ def cast_speed(speed: int) -> int:
"charger_voltage": ("charger_voltage", int),
"time_to_full_charge": ("time_to_full_charge", float),
"charge_limit_soc": ("charge_limit_soc", int),
"plugged_in": ("charging_state", cast_plugged_in),
"charge_port_door_open": ("charge_port_door_open", cast_bool),
"charge_current_request": ("charge_current_request", int),
"charge_current_request_max": ("charge_current_request_max", int),
Expand Down Expand Up @@ -316,68 +349,63 @@ async def async_handle_new_data(self, msg: ReceiveMessage):

if mqtt_attr in MAP_DRIVE_STATE:
attr, cast = MAP_DRIVE_STATE[mqtt_attr]
self.update_drive_state(car, attr, cast(msg.payload))
coordinator.async_update_listeners_debounced()
self.update_car_state(car, "drive_state", attr, cast(msg.payload))

elif mqtt_attr in MAP_VEHICLE_STATE:
attr, cast = MAP_VEHICLE_STATE[mqtt_attr]
self.update_vehicle_state(car, attr, cast(msg.payload))
coordinator.async_update_listeners_debounced()
self.update_car_state(car, "vehicle_state", attr, cast(msg.payload))

elif mqtt_attr in MAP_CLIMATE_STATE:
attr, cast = MAP_CLIMATE_STATE[mqtt_attr]
self.update_climate_state(car, attr, cast(msg.payload))
coordinator.async_update_listeners_debounced()
self.update_car_state(car, "climate_state", attr, cast(msg.payload))

elif mqtt_attr in MAP_CHARGE_STATE:
attr, cast = MAP_CHARGE_STATE[mqtt_attr]
self.update_charge_state(car, attr, cast(msg.payload))
coordinator.async_update_listeners_debounced()
self.update_car_state(car, "charge_state", attr, cast(msg.payload))

@staticmethod
def update_drive_state(car: TeslaCar, attr, value):
"""Update Drive State Safely."""
# pylint: disable=protected-access
logger.debug("Updating drive_state for VIN:%s", car.vin)
elif mqtt_attr == "plugged_in":
self.update_charging_state(car, cast_plugged_in(msg.payload, car))

if "drive_state" not in car._vehicle_data:
car._vehicle_data["drive_state"] = {}
elif mqtt_attr == "state":
(state, charging_state) = cast_car_state(msg.payload, car)
self.update_charging_state(car, charging_state)
self.update_car_state(car, None, "state", state)

drive_state = car._vehicle_data["drive_state"]
drive_state[attr] = value

@staticmethod
def update_vehicle_state(car, attr, value):
"""Update Vehicle State Safely."""
# pylint: disable=protected-access
logger.debug("Updating vehicle_state for VIN:%s", car.vin)
else:
# Nothing matched. Return without updating listeners.
return

if "vehicle_state" not in car._vehicle_data:
car._vehicle_data["vehicle_state"] = {}
coordinator.async_update_listeners_debounced()

vehicle_state = car._vehicle_data["vehicle_state"]
vehicle_state[attr] = value
def update_charging_state(self, car: TeslaCar, val: str):
"""Update charging state."""
self.update_car_state(car, "charge_state", "charging_state", val)

@staticmethod
def update_climate_state(car, attr, value):
"""Update Climate State Safely."""
def update_car_state(car: TeslaCar, sub_path: str, attr: str, value):
"""Update state safely."""
# pylint: disable=protected-access
logger.debug("Updating climate_state for VIN:%s", car.vin)

if "climate_state" not in car._vehicle_data:
car._vehicle_data["climate_state"] = {}
if sub_path is not None:
logger.debug(
"Updating state '%s' to value '%s' in sub_path '%s' for VIN:%s",
attr,
value,
sub_path,
car.vin,
)

climate_state = car._vehicle_data["climate_state"]
climate_state[attr] = value
if sub_path not in car._vehicle_data:
car._vehicle_data[sub_path] = {}

@staticmethod
def update_charge_state(car, attr, value):
"""Update Charge State Safely."""
# pylint: disable=protected-access
logger.debug("Updating charge_state for VIN:%s", car.vin)

if "charge_state" not in car._vehicle_data:
car._vehicle_data["charge_state"] = {}

charge_state = car._vehicle_data["charge_state"]
charge_state[attr] = value
state = car._vehicle_data[sub_path]
state[attr] = value
else:
logger.debug(
"Updating state '%s' to value '%s' in root for VIN:%s",
attr,
value,
car.vin,
)
state = car._car
state[attr] = value
Loading

0 comments on commit 620aacb

Please sign in to comment.