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 independent sensors and remove attributes from device_tracker #30

Merged
merged 14 commits into from
Dec 2, 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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ _Component to integrate with [leafspy][leafspy]._

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
`device_tracker` | Track a Nissan Leaf using the Leaf Spy app.
`sensor` | All data about the vehicle sent from Leaf Spy is displayed as independent sensors. They won't appear in Home Assistant until data is received.
`binary_sensor` | Power switch sensor.

![leafspy][leafspyimg]

Expand All @@ -40,6 +42,8 @@ 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/sensor.py
custom_components/leafspy/binary_sensor.py
custom_components/leafspy/manifest.json
custom_components/leafspy/strings.json
```
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
129 changes: 129 additions & 0 deletions custom_components/leafspy/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""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.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."""
value_fn: Callable[[dict], Any] = field(default=lambda data: None)
transform_fn: Callable[[Any], bool] = field(default=lambda x: bool(x))

BINARY_SENSOR_TYPES = [
LeafSpyBinarySensorDescription(
key="power switch",
translation_key="power_switch_state",
device_class=BinarySensorDeviceClass.POWER,
value_fn=lambda data: data.get("PwrSw"),
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)
_LOGGER.debug("Binary Sensor '%s': Raw data=%s, Parsed value=%s", description.key, message, value)

value = description.transform_fn(value)
_LOGGER.debug("Binary Sensor '%s': Transformed value=%s", description.key, value)

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

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

async_dispatcher_connect(hass, DOMAIN, _process_message)
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.entity_description = description

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

@property
def name(self):
"""Return the name of the binary sensor."""
return f"Leaf {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 {
"identifiers": {(DOMAIN, self._device_id)},
}

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()
if last_state:
try:
transform_fn = self.entity_description.transform_fn
self._value = transform_fn(last_state.state)
except (ValueError, TypeError):
_LOGGER.warning(f"Could not restore state for {self.name}")
41 changes: 1 addition & 40 deletions custom_components/leafspy/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,6 @@

_LOGGER = logging.getLogger(__name__)

PLUG_STATES = [
"Not Plugged In",
"Partially Plugged In",
"Plugged In"
]

CHARGE_MODES = [
"Not Charging",
"Level 1 Charging (100-120 Volts)",
"Level 2 Charging (200-240 Volts)",
"Level 3 Quick Charging"
]


async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Leaf Spy based off an entry."""
async def _receive_data(dev_id, **data):
Expand Down Expand Up @@ -90,11 +76,6 @@ def battery_level(self):
"""Return the battery level of the car."""
return self._data.get('battery_level')

@property
def extra_state_attributes(self):
"""Return extra attributes."""
return self._data.get('attributes')

@property
def latitude(self):
"""Return latitude value of the car."""
Expand Down Expand Up @@ -147,8 +128,6 @@ async def async_added_to_hass(self):
'latitude': attr.get(ATTR_LATITUDE),
'longitude': attr.get(ATTR_LONGITUDE),
'battery_level': attr.get(ATTR_BATTERY_LEVEL),
'attributes': attr

}

@callback
Expand All @@ -166,25 +145,7 @@ def _parse_see_args(message):
'device_name': message['user'],
'latitude': float(message['Lat']),
'longitude': float(message['Long']),
'battery_level': float(message['SOC']),
'attributes': {
'amp_hours': float(message['AHr']),
'trip': int(message['Trip']),
'odometer': float(message['Odo']),
'battery_temperature': float(message['BatTemp']),
'battery_health': float(message['SOH']),
'outside_temperature': float(message['Amb']),
'plug_state': PLUG_STATES[int(message['PlugState'])],
'charge_mode': CHARGE_MODES[int(message['ChrgMode'])],
'charge_power': int(message['ChrgPwr']),
'vin': message['VIN'],
'power_switch': message['PwrSw'] == '1',
'device_battery': int(message['DevBat']),
'rpm': int(message['RPM']),
'gids': int(message['Gids']),
'elevation': float(message['Elv']),
'sequence': int(message['Seq'])
}
'battery_level': float(message['SOC'])
}

return args
Expand Down
Loading