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 extra sensors for BMW ConnectedDrive #12591

Merged
merged 10 commits into from
Mar 15, 2018
117 changes: 117 additions & 0 deletions homeassistant/components/binary_sensor/bmw_connected_drive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""
Reads vehicle status from BMW connected drive portal.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.bmw_connected_drive/
"""
import asyncio
import logging

from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.components.binary_sensor import BinarySensorDevice

DEPENDENCIES = ['bmw_connected_drive']

_LOGGER = logging.getLogger(__name__)

SENSOR_TYPES = {
'all_lids_closed': ['Doors', 'opening'],
'all_windows_closed': ['Windows', 'opening'],
'door_lock_state': ['Door lock state', 'safety']
}


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the BMW sensors."""
accounts = hass.data[BMW_DOMAIN]
_LOGGER.debug('Found BMW accounts: %s',
', '.join([a.name for a in accounts]))
devices = []
for account in accounts:
for vehicle in account.account.vehicles:
for key, value in sorted(SENSOR_TYPES.items()):
device = BMWConnectedDriveSensor(account, vehicle, key,
value[0], value[1])
devices.append(device)
add_devices(devices, True)


class BMWConnectedDriveSensor(BinarySensorDevice):
"""Representation of a BMW vehicle binary sensor."""

def __init__(self, account, vehicle, attribute: str, sensor_name,
device_class):
"""Constructor."""
self._account = account
self._vehicle = vehicle
self._attribute = attribute
self._name = sensor_name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the name of the sensor also contain the name of the vehicle?
If someone has more than one vehicle, we need to make sure that the sensors then get different names...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vehicle name is not show in the sensor name, but it is shown in the attributes. Not sure what is the best approach, but maybe those people with more than one vehicle can update the friendly name of the sensor? Because in my opinion it's not necessary to show the vehicle name in the sensor name if you have one BMW (which will be the majority I guess).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an option where people can give names to their vehicles? Something like:

bmw_connected_drive:
  my_account:
    username: USERNAME_BMW_CONNECTED_DRIVE
    password: PASSWORD_BMW_CONNECTED_DRIVE
    country: COUNTRY_BMW_CONNECTED_DRIVE
    vehicles:
    - vin: SOMEVIN
      name: his
    - vin: SOMEOTHERVIN
      name: hers

That way people can

  • select which cars should be listed in HASS at all
  • set custom names for their vehicles
  • we have different names for the different vehicles?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is indeed something we could add as an option.

self._device_class = device_class
self._state = None

@property
def should_poll(self) -> bool:
"""Data update is triggered from BMWConnectedDriveEntity."""
return False

@property
def name(self):
"""Return the name of the binary sensor."""
return self._name

@property
def device_class(self):
"""Return the class of the binary sensor."""
return self._device_class

@property
def is_on(self):
"""Return the state of the binary sensor."""
return self._state

@property
def device_state_attributes(self):
"""Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state
result = {
'car': self._vehicle.modelName
}

if self._attribute == 'all_lids_closed':
for lid in vehicle_state.lids:
result[lid.name] = lid.state.value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is lid name lowercase snakecase? That's the home assistant standard.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, this is an example of a lid name: door_driver_front.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

elif self._attribute == 'all_windows_closed':
for window in vehicle_state.windows:
result[window.name] = window.state.value
elif self._attribute == 'door_lock_state':
result['door_lock_state'] = vehicle_state.door_lock_state.value

return result

def update(self):
"""Read new state data from the library."""
vehicle_state = self._vehicle.state

# device class opening: On means open, Off means closed
if self._attribute == 'all_lids_closed':
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
self._state = not vehicle_state.all_lids_closed
if self._attribute == 'all_windows_closed':
self._state = not vehicle_state.all_windows_closed
# device class safety: On means unsafe, Off means safe
if self._attribute == 'door_lock_state':
# Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED
self._state = bool(vehicle_state.door_lock_state.value
in ('SELECTIVELOCKED', 'UNLOCKED'))

def update_callback(self):
"""Schedule a state update."""
self.schedule_update_ha_state(True)

@asyncio.coroutine
def async_added_to_hass(self):
"""Add callback after being added to hass.

Show latest data after startup.
"""
self._account.add_update_listener(self.update_callback)
2 changes: 1 addition & 1 deletion homeassistant/components/bmw_connected_drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
}, extra=vol.ALLOW_EXTRA)


BMW_COMPONENTS = ['device_tracker', 'sensor']
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
UPDATE_INTERVAL = 5 # in minutes


Expand Down
108 changes: 108 additions & 0 deletions homeassistant/components/lock/bmw_connected_drive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
Support for BMW cars with BMW ConnectedDrive.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/lock.bmw_connected_drive/
"""
import asyncio
import logging

from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.components.lock import LockDevice
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED

DEPENDENCIES = ['bmw_connected_drive']

_LOGGER = logging.getLogger(__name__)


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the BMW Connected Drive lock."""
accounts = hass.data[BMW_DOMAIN]
_LOGGER.debug('Found BMW accounts: %s',
', '.join([a.name for a in accounts]))
devices = []
for account in accounts:
for vehicle in account.account.vehicles:
device = BMWLock(account, vehicle, 'lock', 'BMW lock')
devices.append(device)
add_devices(devices, True)


class BMWLock(LockDevice):
"""Representation of a BMW vehicle lock."""

def __init__(self, account, vehicle, attribute: str, sensor_name):
"""Initialize the lock."""
self._account = account
self._vehicle = vehicle
self._attribute = attribute
self._name = sensor_name
self._state = None

@property
def should_poll(self):
"""Do not poll this class.

Updates are triggered from BMWConnectedDriveAccount.
"""
return False

@property
def name(self):
"""Return the name of the lock."""
return self._name

@property
def device_state_attributes(self):
"""Return the state attributes of the lock."""
vehicle_state = self._vehicle.state
return {
'car': self._vehicle.modelName,
'door_lock_state': vehicle_state.door_lock_state.value
}

@property
def is_locked(self):
"""Return true if lock is locked."""
return self._state == STATE_LOCKED

def lock(self, **kwargs):
"""Lock the car."""
_LOGGER.debug("%s: locking doors", self._vehicle.modelName)
# Optimistic state set here because it takes some time before the
# update callback response
self._state = STATE_LOCKED
self.schedule_update_ha_state()
self._vehicle.remote_services.trigger_remote_door_lock()

def unlock(self, **kwargs):
"""Unlock the car."""
_LOGGER.debug("%s: unlocking doors", self._vehicle.modelName)
# Optimistic state set here because it takes some time before the
# update callback response
self._state = STATE_UNLOCKED
self.schedule_update_ha_state()
self._vehicle.remote_services.trigger_remote_door_unlock()

def update(self):
"""Update state of the lock."""
_LOGGER.debug("%s: updating data for %s", self._vehicle.modelName,
self._attribute)
vehicle_state = self._vehicle.state

# Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED
self._state = (STATE_LOCKED if vehicle_state.door_lock_state.value
in ('LOCKED', 'SECURED') else STATE_UNLOCKED)

def update_callback(self):
"""Schedule a state update."""
self.schedule_update_ha_state(True)

@asyncio.coroutine
def async_added_to_hass(self):
"""Add callback after being added to hass.

Show latest data after startup.
"""
self._account.add_update_listener(self.update_callback)
49 changes: 33 additions & 16 deletions homeassistant/components/sensor/bmw_connected_drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@

_LOGGER = logging.getLogger(__name__)

LENGTH_ATTRIBUTES = [
'remaining_range_fuel',
'mileage',
]
LENGTH_ATTRIBUTES = {
'remaining_range_fuel': ['Range (fuel)', 'mdi:ruler'],
'mileage': ['Mileage', 'mdi:speedometer']
}

VALID_ATTRIBUTES = LENGTH_ATTRIBUTES + [
'remaining_fuel',
]
VALID_ATTRIBUTES = {
'remaining_fuel': ['Remaining Fuel', 'mdi:gas-station']
}

VALID_ATTRIBUTES.update(LENGTH_ATTRIBUTES)


def setup_platform(hass, config, add_devices, discovery_info=None):
Expand All @@ -32,23 +34,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
for account in accounts:
for vehicle in account.account.vehicles:
for sensor in VALID_ATTRIBUTES:
device = BMWConnectedDriveSensor(account, vehicle, sensor)
for key, value in sorted(VALID_ATTRIBUTES.items()):
device = BMWConnectedDriveSensor(account, vehicle, key,
value[0], value[1])
devices.append(device)
add_devices(devices)
add_devices(devices, True)


class BMWConnectedDriveSensor(Entity):
"""Representation of a BMW vehicle sensor."""

def __init__(self, account, vehicle, attribute: str):
def __init__(self, account, vehicle, attribute: str, sensor_name, icon):
"""Constructor."""
self._vehicle = vehicle
self._account = account
self._attribute = attribute
self._state = None
self._unit_of_measurement = None
self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
self._name = sensor_name
self._icon = icon

@property
def should_poll(self) -> bool:
Expand All @@ -60,6 +64,11 @@ def name(self) -> str:
"""Return the name of the sensor."""
return self._name

@property
def icon(self):
"""Icon to use in the frontend, if any."""
return self._icon

@property
def state(self):
"""Return the state of the sensor.
Expand All @@ -74,9 +83,16 @@ def unit_of_measurement(self) -> str:
"""Get the unit of measurement."""
return self._unit_of_measurement

@property
def device_state_attributes(self):
"""Return the state attributes of the binary sensor."""
return {
'car': self._vehicle.modelName
}

def update(self) -> None:
"""Read new state data from the library."""
_LOGGER.debug('Updating %s', self.entity_id)
_LOGGER.debug('Updating %s', self._vehicle.modelName)
vehicle_state = self._vehicle.state
self._state = getattr(vehicle_state, self._attribute)

Expand All @@ -87,13 +103,14 @@ def update(self) -> None:
else:
self._unit_of_measurement = None

self.schedule_update_ha_state()
def update_callback(self):
"""Schedule a state update."""
self.schedule_update_ha_state(True)

@asyncio.coroutine
def async_added_to_hass(self):
"""Add callback after being added to hass.

Show latest data after startup.
"""
self._account.add_update_listener(self.update)
yield from self.hass.async_add_job(self.update)
self._account.add_update_listener(self.update_callback)