Skip to content

Commit

Permalink
Add independent sensors and remove attributes from device_tracker (#30)
Browse files Browse the repository at this point in the history
Add proper sensors and binary_sensors to represent information previously stored as attributes in the device_tracker. Some outstanding issues still, so this is a work in progress.
  • Loading branch information
Chaoscontrol authored Dec 2, 2024
1 parent 0168db7 commit 12b1bb3
Show file tree
Hide file tree
Showing 5 changed files with 503 additions and 43 deletions.
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

0 comments on commit 12b1bb3

Please sign in to comment.