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

chore: release 2023-10-28 #754

Merged
merged 5 commits into from
Oct 29, 2023
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
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
Loading