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 proper sensor and binary sensor entities #39

Merged
merged 38 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
12b1bb3
Add independent sensors and remove attributes from device_tracker (#30)
Chaoscontrol Dec 2, 2024
32b9494
Checking for 1 as a string to fix the binary sensor (#34)
Chaoscontrol Dec 2, 2024
6e9c36f
Sensors to initialise automatically after HA restart and recover last…
Chaoscontrol Dec 2, 2024
fa13d17
edit setup instructions
wtadler Dec 8, 2024
fb8b72a
fix binary_sensor restoration
wtadler Dec 8, 2024
3689496
clean up sensor names/icons
wtadler Dec 8, 2024
a142b1f
remove icon from power binary sensor
wtadler Dec 10, 2024
581e0ad
Revert "remove icon from power binary sensor"
wtadler Dec 10, 2024
9fc80d4
(SensorEntity, RestoreEntity) -> RestoreSensor
wtadler Dec 13, 2024
f9a4cab
Hx in percentage units
wtadler Dec 13, 2024
29c5530
partial readme update
wtadler Dec 13, 2024
51ab262
continue drafting readme
wtadler Dec 14, 2024
20850a5
eliminate transform_fn, unit_fn, temp conversion
wtadler Dec 15, 2024
bc64f16
remove transform_fn from binary_sensor
wtadler Dec 15, 2024
3836127
documentation updates
wtadler Dec 15, 2024
fe839fc
Add device name for all entities and car icon for device tracker (#38)
Chaoscontrol Dec 17, 2024
728971b
refactor entity keys/names
wtadler Dec 18, 2024
eac93d2
rename devicetracker class and entity_Id
wtadler Dec 18, 2024
ebd54e7
patch to transform Hx
wtadler Dec 18, 2024
4572b3e
default device_tracker name
wtadler Dec 18, 2024
870dc44
updated setup instructions
wtadler Dec 18, 2024
fbea2ee
modify keying, bring back transform_fn
wtadler Dec 18, 2024
fc2253b
little fixes
wtadler Dec 18, 2024
38fed7b
binary sensor restart fix
wtadler Dec 18, 2024
a277af5
Update README.md
wtadler Dec 18, 2024
bf89cfb
Update README.md
wtadler Dec 18, 2024
445a0f9
Update README.md
wtadler Dec 18, 2024
e585896
round odometer
wtadler Dec 19, 2024
eae18a7
Update README.md
wtadler Dec 19, 2024
d1a71c0
setup instructions
wtadler Dec 19, 2024
9363bb6
version number bump
wtadler Dec 19, 2024
2c8a816
Merge branch 'sensor_entities' of https://github.com/jesserockz/ha-le…
wtadler Dec 19, 2024
aeeb5b7
Merge branch 'main' into sensor_entities
wtadler Dec 19, 2024
5344f97
Update README.md
wtadler Dec 19, 2024
f900cf7
add leafspy_key
wtadler Dec 19, 2024
d251565
Merge branch 'sensor_entities' of https://github.com/jesserockz/ha-le…
wtadler Dec 19, 2024
d844c4a
alphabetize sensors
wtadler Dec 19, 2024
9fd64fd
round odometer
wtadler Dec 19, 2024
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
119 changes: 62 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# leafspy
# LeafSpy integration for Home Assistant

[![GitHub Release][releases-shield]][releases]
[![GitHub Activity][commits-shield]][commits]
Expand All @@ -7,75 +7,80 @@
[![hacs][hacsbadge]][hacs]
![Project Maintenance][maintenance-shield]

[![Discord][discord-shield]][discord]
[![Community Forum][forum-shield]][forum]
<p align="center"><img src="leafspy.png" width="64"></p>

_Component to integrate with [leafspy][leafspy]._
This Home Assistant component enables you to get information from your Nissan Leaf car into Home Assistant by integrating with the LeafSpy [Android](https://play.google.com/store/apps/details?id=com.Turbo3.Leaf_Spy_Pro&hl=en_US) or [iOS](https://apps.apple.com/us/app/leafspy-pro/id967376861) apps.

**This component will set up the following platforms.**
Plug a Bluetooth OBD2 adapter (like [this one](https://www.amazon.com/gp/product/B0755N61PW/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)) into your Nissan Leaf. Open up LeafSpy and pair your phone with the adapter. It will then read information from your car. This integration will allow your phone to submit that info to Home Assistant.

Platform | Description
-- | --
`device_tracker` | Track a Nissan Leaf using the Leaf Spy app. Data about the vehicle sent from Leaf Spy will be viewable under Attributes
## Installation and configuration

![leafspy][leafspyimg]

## Installation
1. [Install HACS](https://www.hacs.xyz/docs/use/configuration/basic/).
2. Add this to HACS as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories/) by pressing this button and pressing "Download" in the bottom right.

[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=jesserockz&repository=ha-leafspy&category=integration)

1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`).
2. If you do not have a `custom_components` directory (folder) there, you need to create it.
3. In the `custom_components` directory (folder) create a new folder called `leafspy`.
4. Download _all_ the files from the `custom_components/leafspy/` directory (folder) in this repository.
5. Place the files you downloaded in the new directory (folder) you created.
6. Restart Home Assistant
7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Leaf Spy"

Using your HA configuration directory (folder) as a starting point you should now also have this:

```text
custom_components/leafspy/translations/en.json
custom_components/leafspy/__init__.py
custom_components/leafspy/config_flow.py
custom_components/leafspy/const.py
custom_components/leafspy/device_tracker.py
custom_components/leafspy/manifest.json
custom_components/leafspy/strings.json
```

## Configuration is done in the UI

### Android/iOS app configuration

Open the Leaf Spy app, go to `Menu` -> `Settings` and scroll down to `Server`.
Change the following settings:
- `Enable`: Yes
- `Send Interval` can be whatever you prefer.
- `ID`: `<Car Name>`
- `PW`: secret generated when the integration was installed
- `Http` or `Https` depending on the access to your Home Assistant install.
- `URL`: webhook url generated when the integration was installed
- Note: **Do not** add http or https to the URL.

<!---->

## Contributions are welcome!

If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)
3. Restart Home Assistant
4. Add the Leaf Spy integration by pressing this button.

[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=leafspy)

5. Configure your LeafSpy app settings using the information below, which will also be displayed on screen. If you don't have the app in front of you, copy down the password. The generated password will only be shown this once; if you don't copy it down, you'll have to uninstall the integration, restart, and reinstall. Open the LeafSpy app, go to `Menu` -> `Settings`.
- In the **Units** section:
- Choose `°C`
- `Convert Outside Temperature`: `On`
- `CAN Odometer in Miles`: `On` (if you see the option and if your car odometer displays in miles)
- In the **Server** section:
- `Enable`: `On`
- `Send Interval`: Whatever frequency you prefer
- `PW`: `<Generated and displayed during setup>`
- `Http://` or `Https://`: Depends on your Home Assistant installation
- `URL`: `<Displayed during setup>`
- (**Do not** include the http or https prefix in the URL field.)

## Entities
_See [LeafSpy manual](https://leafspy.com/wp-content/uploads/2024/04/LeafSpy-Help-1.5.0.pdf#page=70) for more details on the data that the app sends._

### Device tracker
| Entity ID | Note |
| :-- | :-- |
| device_tracker.leaf | Tracks latitude, longitude, GPS accuracy, and battery level |

### Binary sensor
| Entity ID |
| :-- |
| binary_sensor.leaf_power |

### Sensors
| Entity ID | Unit reported by LeafSpy | Note |
| :-- | :-- | :-- |
| sensor.leaf_ambient_temperature | °C (You must set this in the LeafSpy app; see instructions above.) | Unit adjustable in HA UI. |
| sensor.leaf_battery_capacity | Ah | |
| sensor.leaf_battery_conductance | % | Referred to as Hx in the LeafSpy manual. |
| sensor.leaf_battery_current | A | |
| sensor.leaf_battery_health | % | |
| sensor.leaf_battery_state_of_charge | % | |
| sensor.leaf_battery_stat_of_charge_gids | Gids | |
| sensor.leaf_battery_temperature | °C (You must set this in the LeafSpy app; see instructions above.) | Unit adjustable in HA UI. |
| sensor.leaf_battery_voltage | V | |
| sensor.leaf_charge_mode | --- | |
| sensor.leaf_charge_power | W | Not very accurate. For example, when charging via level 2 charging, it just guesses 6,000 W. |
| sensor.leaf_elevation | m | Unit adjustable in HA UI |
| sensor.leaf_front_wiper_status | --- | To get this information you may need to make a custom screen in LeafSpy to read wiper status. |
| sensor.leaf_motor_speed | RPM | |
| sensor.leaf_odometer | km (You must indicate in LeafSpy if your displayed car odometer is in mi; see instructions above.) | Unit later adjustable in HA UI. |
| sensor.leaf_phone_battery | % | |
| sensor.leaf_plug_status | --- | Reports "Not plugged", "Partial Plugged", or "Plugged." |
| sensor.leaf_sequence_number | --- | A number that increments with each report from LeafSpy. |
| sensor.leaf_speed | km/h | Unit adjustable in HA UI. |
| sensor.leaf_trip_number | --- | Tracks total number of trips taken. |
| sensor.leaf_vin | --- | Car unique identifier. |

***

[leafspy]: https://play.google.com/store/apps/details?id=com.Turbo3.Leaf_Spy_Pro
[commits-shield]: https://img.shields.io/github/commit-activity/y/jesserockz/ha-leafspy.svg?style=for-the-badge
[commits]: https://github.com/jesserockz/ha-leafspy/commits/main
[hacs]: https://github.com/custom-components/hacs
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
[discord]: https://discord.gg/Qa5fW2R
[discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge
[leafspyimg]: leafspy.png
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge
[forum]: https://community.home-assistant.io/
[license-shield]: https://img.shields.io/github/license/jesserockz/ha-leafspy.svg?style=for-the-badge
[maintenance-shield]: https://img.shields.io/badge/maintainer-Will%20Adler%20%40wtadler-blue.svg?style=for-the-badge
[releases-shield]: https://img.shields.io/github/release/jesserockz/ha-leafspy.svg?style=for-the-badge
Expand Down
5 changes: 3 additions & 2 deletions custom_components/leafspy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

_LOGGER = logging.getLogger(__name__)

PLATFORMS = ["device_tracker"]
PLATFORMS = ["device_tracker", "sensor", "binary_sensor"]

# Use empty_config_schema because the component does not have any config options
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
Expand All @@ -28,6 +28,7 @@ async def async_setup(hass, config):
"""Initialize Leaf Spy component."""
hass.data[DOMAIN] = {
'devices': {},
'sensors': {},
'unsub': None,
}
return True
Expand All @@ -44,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.http.register_view(LeafSpyView())

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

hass.data[DOMAIN]['unsub'] = \
async_dispatcher_connect(hass, DOMAIN, async_handle_message)

Expand Down
172 changes: 172 additions & 0 deletions custom_components/leafspy/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""Binary sensor platform that adds support for Leaf Spy."""
import logging
from dataclasses import dataclass, field
from typing import Any, Callable

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers import device_registry
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

@dataclass(frozen=True)
class LeafSpyBinarySensorDescription(BinarySensorEntityDescription):
"""Describes Leaf Spy binary sensor."""
transform_fn: Callable[[dict], Any] = field(default=lambda x: x)
leafspy_key: str = field(default=None)

BINARY_SENSOR_TYPES = [
LeafSpyBinarySensorDescription(
key="power",
leafspy_key="PwrSw",
device_class=BinarySensorDeviceClass.POWER,
transform_fn=lambda x: x == '1',
icon="mdi:power",
)
]

async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None
) -> bool:
"""Set up Leaf Spy binary sensors based on a config entry."""
if 'binary_sensors' not in hass.data[DOMAIN]:
hass.data[DOMAIN]['binary_sensors'] = {}

async def _process_message(context, message):
"""Process incoming sensor messages."""
try:
_LOGGER.debug("Incoming message: %s", message)
if 'VIN' not in message:
return

dev_id = slugify(f'leaf_{message["VIN"]}')

# Create and update binary sensors for each description
for description in BINARY_SENSOR_TYPES:
sensor_id = f"{dev_id}_{description.key}"
# value = description.value_fn(message)
value = message.get(description.leafspy_key, None)
_LOGGER.debug(f"Binary sensor {description.key}: Initial value={value}")

if description.transform_fn:
value = description.transform_fn(value)
_LOGGER.debug(f"Binary sensor {description.key}: Transformed value={value}")

if value is not None:
sensor = hass.data[DOMAIN]['binary_sensors'].get(sensor_id)

sensor_description = description

if sensor is not None:
sensor.entity_description = sensor_description
sensor.update_state(value)
else:
sensor = LeafSpyBinarySensor(dev_id, description, value)
hass.data[DOMAIN]['binary_sensors'][sensor_id] = sensor
async_add_entities([sensor])

_LOGGER.debug(f"Registered sensor: {sensor.name} with initial value: {value}")

except Exception as err:
_LOGGER.error("Error processing Leaf Spy message: %s", err)

async_dispatcher_connect(hass, DOMAIN, _process_message)

# Restore previously loaded devices
dev_reg = device_registry.async_get(hass)
dev_ids = {
identifier[1]
for device in dev_reg.devices.values()
for identifier in device.identifiers
if identifier[0] == DOMAIN
}

if not dev_ids:
return True

entities = []
for dev_id in dev_ids:

# For each device ID, recreate the sensor entities
for description in BINARY_SENSOR_TYPES:
sensor_id = f"{dev_id}_{description.key}"
sensor = LeafSpyBinarySensor(dev_id, description, False)
hass.data[DOMAIN]['binary_sensors'][sensor_id] = sensor
entities.append(sensor)
async_add_entities(entities)
return True


class LeafSpyBinarySensor(BinarySensorEntity, RestoreEntity):
"""Representation of a Leaf Spy binary sensor."""

def __init__(self, device_id: str, description: LeafSpyBinarySensorDescription, initial_value):
"""Initialize the binary sensor."""
self._device_id = device_id
self._value = initial_value
self._attr_has_entity_name = True
self.entity_description = description

@property
def unique_id(self):
"""Return a unique ID."""
return f"{self._device_id}_{self.entity_description.key}"

@property
def translation_key(self):
"""Return the translation key."""
return self.entity_description.key

@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._value

@property
def device_info(self):
"""Return device information."""
return {
"name": "Leaf",
"identifiers": {(DOMAIN, self._device_id)},
}

@property
def should_poll(self) -> bool:
"""Disable polling for this sensor."""
return False


def update_state(self, new_value):
"""Update the binary sensor state."""
self._value = new_value
self.async_write_ha_state()

async def async_added_to_hass(self):
"""Restore last known state."""
await super().async_added_to_hass()

last_state = await self.async_get_last_state()
_LOGGER.debug(f"Last_state: {last_state}")

# if last_state:
# try:
# self._value = last_state.state
# except (ValueError, TypeError):
# _LOGGER.warning(f"Could not restore state for {self.name}")

# Add this log line to confirm the method is being called
_LOGGER.debug(f"async_added_to_hass called for {self.name}")
Loading
Loading