Skip to content

Commit

Permalink
Add new component: BMW connected drive (#12277)
Browse files Browse the repository at this point in the history
* first working version of BMW connected drive sensor

* extended coveragerc

* fixed blank line

* fixed pylint

* major refactoring after major refactoring in bimmer_connected

* Update are now triggered from BMWConnectedDriveVehicle.
* removed polling from sensor and device_tracker
* backend URL is not detected automatically based on current country
* vehicles are discovered automatically
* updates are async now

resolves:
* bimmerconnected/bimmer_connected#3
* bimmerconnected/bimmer_connected#5

* improved exception handing

* fixed static analysis findings

* fixed review comments from @MartinHjelmare

* improved startup, data is updated right after sensors were created.

* fixed pylint issue

* updated to latest release of the bimmer_connected library

* updated requirements-all.txt

* fixed comments from @MartinHjelmare

* calling self.update from async_add_job

* removed unused attribute "account"
  • Loading branch information
ChristianKuehnel authored and MartinHjelmare committed Feb 20, 2018
1 parent 5d29d88 commit 316eb59
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ omit =
homeassistant/components/arduino.py
homeassistant/components/*/arduino.py

homeassistant/components/bmw_connected_drive.py
homeassistant/components/*/bmw_connected_drive.py

homeassistant/components/android_ip_webcam.py
homeassistant/components/*/android_ip_webcam.py

Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ homeassistant/components/hassio.py @home-assistant/hassio

# Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/ephember.py @ttroy50
homeassistant/components/climate/eq3btsmart.py @rytilahti
Expand Down Expand Up @@ -74,6 +75,7 @@ homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi

homeassistant/components/*/axis.py @kane610
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
Expand Down
105 changes: 105 additions & 0 deletions homeassistant/components/bmw_connected_drive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
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/bmw_connected_drive/
"""
import logging
import datetime

import voluptuous as vol
from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change

import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD
)

REQUIREMENTS = ['bimmer_connected==0.3.0']

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'bmw_connected_drive'
CONF_VALUES = 'values'
CONF_COUNTRY = 'country'

ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_COUNTRY): cv.string,
})

CONFIG_SCHEMA = vol.Schema({
DOMAIN: {
cv.string: ACCOUNT_SCHEMA
},
}, extra=vol.ALLOW_EXTRA)


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


def setup(hass, config):
"""Set up the BMW connected drive components."""
accounts = []
for name, account_config in config[DOMAIN].items():
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
country = account_config[CONF_COUNTRY]
_LOGGER.debug('Adding new account %s', name)
bimmer = BMWConnectedDriveAccount(username, password, country, name)
accounts.append(bimmer)

# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers

now = datetime.datetime.now()
track_utc_time_change(
hass, bimmer.update,
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
second=now.second)

hass.data[DOMAIN] = accounts

for account in accounts:
account.update()

for component in BMW_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)

return True


class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle."""

def __init__(self, username: str, password: str, country: str,
name: str) -> None:
"""Constructor."""
from bimmer_connected.account import ConnectedDriveAccount

self.account = ConnectedDriveAccount(username, password, country)
self.name = name
self._update_listeners = []

def update(self, *_):
"""Update the state of all vehicles.
Notify all listeners about the update.
"""
_LOGGER.debug('Updating vehicle state for account %s, '
'notifying %d listeners',
self.name, len(self._update_listeners))
try:
self.account.update_vehicle_states()
for listener in self._update_listeners:
listener()
except IOError as exception:
_LOGGER.error('Error updating the vehicle state.')
_LOGGER.exception(exception)

def add_update_listener(self, listener):
"""Add a listener for update notifications."""
self._update_listeners.append(listener)
51 changes: 51 additions & 0 deletions homeassistant/components/device_tracker/bmw_connected_drive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Device tracker for BMW Connected Drive vehicles.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.bmw_connected_drive/
"""
import logging

from homeassistant.components.bmw_connected_drive import DOMAIN \
as BMW_DOMAIN
from homeassistant.util import slugify

DEPENDENCIES = ['bmw_connected_drive']

_LOGGER = logging.getLogger(__name__)


def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the BMW tracker."""
accounts = hass.data[BMW_DOMAIN]
_LOGGER.debug('Found BMW accounts: %s',
', '.join([a.name for a in accounts]))
for account in accounts:
for vehicle in account.account.vehicles:
tracker = BMWDeviceTracker(see, vehicle)
account.add_update_listener(tracker.update)
tracker.update()
return True


class BMWDeviceTracker(object):
"""BMW Connected Drive device tracker."""

def __init__(self, see, vehicle):
"""Initialize the Tracker."""
self._see = see
self.vehicle = vehicle

def update(self) -> None:
"""Update the device info."""
dev_id = slugify(self.vehicle.modelName)
_LOGGER.debug('Updating %s', dev_id)
attrs = {
'trackr_id': dev_id,
'id': dev_id,
'name': self.vehicle.modelName
}
self._see(
dev_id=dev_id, host_name=self.vehicle.modelName,
gps=self.vehicle.state.gps_position, attributes=attrs,
icon='mdi:car'
)
99 changes: 99 additions & 0 deletions homeassistant/components/sensor/bmw_connected_drive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
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/sensor.bmw_connected_drive/
"""
import logging
import asyncio

from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.helpers.entity import Entity

DEPENDENCIES = ['bmw_connected_drive']

_LOGGER = logging.getLogger(__name__)

LENGTH_ATTRIBUTES = [
'remaining_range_fuel',
'mileage',
]

VALID_ATTRIBUTES = LENGTH_ATTRIBUTES + [
'remaining_fuel',
]


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 sensor in VALID_ATTRIBUTES:
device = BMWConnectedDriveSensor(account, vehicle, sensor)
devices.append(device)
add_devices(devices)


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

def __init__(self, account, vehicle, attribute: str):
"""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)

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

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

@property
def state(self):
"""Return the state of the sensor.
The return type of this call depends on the attribute that
is configured.
"""
return self._state

@property
def unit_of_measurement(self) -> str:
"""Get the unit of measurement."""
return self._unit_of_measurement

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

if self._attribute in LENGTH_ATTRIBUTES:
self._unit_of_measurement = vehicle_state.unit_of_length
elif self._attribute == 'remaining_fuel':
self._unit_of_measurement = vehicle_state.unit_of_volume
else:
self._unit_of_measurement = None

self.schedule_update_ha_state()

@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)
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ beautifulsoup4==4.6.0
# homeassistant.components.zha
bellows==0.5.0

# homeassistant.components.bmw_connected_drive
bimmer_connected==0.3.0

# homeassistant.components.blink
blinkpy==0.6.0

Expand Down

0 comments on commit 316eb59

Please sign in to comment.