From d13bcf8412a74de965ab3bede8c85483c12207f5 Mon Sep 17 00:00:00 2001 From: Gerard <gerard33@users.noreply.github.com> Date: Thu, 15 Mar 2018 22:56:35 +0100 Subject: [PATCH] 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 --- .../binary_sensor/bmw_connected_drive.py | 117 ++++++++++++++++++ .../components/bmw_connected_drive.py | 2 +- .../components/lock/bmw_connected_drive.py | 108 ++++++++++++++++ .../components/sensor/bmw_connected_drive.py | 49 +++++--- 4 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/binary_sensor/bmw_connected_drive.py create mode 100644 homeassistant/components/lock/bmw_connected_drive.py diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py new file mode 100644 index 00000000000..0c848a57fbf --- /dev/null +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -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) diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive.py index 86048a56e22..9e9e2bafac5 100644 --- a/homeassistant/components/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive.py @@ -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 diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py new file mode 100644 index 00000000000..4592fd7cae9 --- /dev/null +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -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) diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 26bfd19e6fc..76719763931 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -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) -- GitLab