Add extra sensors for BMW ConnectedDrive (#12591)

* Added extra sensors for BMW ConnectedDrive

* Updates based on review of @MartinHjelmare

* Updates based on 2nd review of @MartinHjelmare

* Changed control flow for updates to support updates triggered by remote services.

* updated library version number

* Changed order of commands so that the UI looks consistent.

State of lock is now set optimisitcally before getting proper update
from the server. So that the state does not toggle in the UI.

* Added comment on optimistic state

* Updated requirements_all.txt

* Revert access permission changes

* Fix for Travis

* Changes based on review by @MartinHjelmare
This commit is contained in:
Gerard 2018-03-15 22:56:35 +01:00 committed by Martin Hjelmare
parent 456ff4e84b
commit d13bcf8412
4 changed files with 259 additions and 17 deletions

View File

@ -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
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
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)

View File

@ -37,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
BMW_COMPONENTS = ['device_tracker', 'sensor']
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
UPDATE_INTERVAL = 5 # in minutes

View File

@ -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)

View File

@ -14,14 +14,16 @@ DEPENDENCIES = ['bmw_connected_drive']
_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):
@ -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:
@ -60,6 +64,11 @@ class BMWConnectedDriveSensor(Entity):
"""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.
@ -74,9 +83,16 @@ class BMWConnectedDriveSensor(Entity):
"""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)
@ -87,7 +103,9 @@ class BMWConnectedDriveSensor(Entity):
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):
@ -95,5 +113,4 @@ class BMWConnectedDriveSensor(Entity):
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)