mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 06:17:07 +00:00
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"
This commit is contained in:
parent
5d29d88888
commit
316eb59de2
@ -29,6 +29,9 @@ omit =
|
|||||||
homeassistant/components/arduino.py
|
homeassistant/components/arduino.py
|
||||||
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
|
||||||
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
|
# Individual components
|
||||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||||
|
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
||||||
homeassistant/components/camera/yi.py @bachya
|
homeassistant/components/camera/yi.py @bachya
|
||||||
homeassistant/components/climate/ephember.py @ttroy50
|
homeassistant/components/climate/ephember.py @ttroy50
|
||||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
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/xiaomi_aqara.py @danielhiversen @syssi
|
||||||
|
|
||||||
homeassistant/components/*/axis.py @kane610
|
homeassistant/components/*/axis.py @kane610
|
||||||
|
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
||||||
homeassistant/components/*/broadlink.py @danielhiversen
|
homeassistant/components/*/broadlink.py @danielhiversen
|
||||||
homeassistant/components/hive.py @Rendili @KJonline
|
homeassistant/components/hive.py @Rendili @KJonline
|
||||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||||
|
105
homeassistant/components/bmw_connected_drive.py
Normal file
105
homeassistant/components/bmw_connected_drive.py
Normal file
@ -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)
|
@ -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
homeassistant/components/sensor/bmw_connected_drive.py
Normal file
99
homeassistant/components/sensor/bmw_connected_drive.py
Normal file
@ -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)
|
@ -135,6 +135,9 @@ beautifulsoup4==4.6.0
|
|||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
bellows==0.5.0
|
bellows==0.5.0
|
||||||
|
|
||||||
|
# homeassistant.components.bmw_connected_drive
|
||||||
|
bimmer_connected==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.blink
|
# homeassistant.components.blink
|
||||||
blinkpy==0.6.0
|
blinkpy==0.6.0
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user