Skip to content
Snippets Groups Projects
Commit 316eb59d authored by ChristianKuehnel's avatar ChristianKuehnel Committed by Martin Hjelmare
Browse files

Add new component: BMW connected drive (#12277)

* 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:
* https://github.com/ChristianKuehnel/bimmer_connected/issues/3
* https://github.com/ChristianKuehnel/bimmer_connected/issues/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"
parent 5d29d888
No related branches found
No related tags found
No related merge requests found
......@@ -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
......
......@@ -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
......@@ -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
......
"""
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)
"""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'
)
"""
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)
......@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment