Skip to content

Commit

Permalink
ConfigFlow for HomeAssistant
Browse files Browse the repository at this point in the history
  • Loading branch information
rikroe committed May 9, 2020
1 parent 4ba9402 commit f309ef0
Show file tree
Hide file tree
Showing 16 changed files with 808 additions and 545 deletions.
222 changes: 222 additions & 0 deletions custom_components/bmw_connected_drive/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
"""Reads vehicle status from BMW connected drive portal."""
import asyncio
from datetime import timedelta
import logging
import json

import async_timeout
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, ServiceRegistry
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import homeassistant.util.dt as dt_util

_LOGGER = logging.getLogger(__name__)

DOMAIN = "bmw_connected_drive"
CONF_REGION = "region"
CONF_READ_ONLY = "read_only"
ATTR_VIN = "vin"

ACCOUNT_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_REGION): vol.Any("north_america", "china", "rest_of_world"),
vol.Optional(CONF_READ_ONLY, default=False): cv.boolean,
}
)

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

SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string})


BMW_COMPONENTS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"]
UPDATE_INTERVAL = 5 # in minutes

SERVICE_UPDATE_STATE = "update_state"

_SERVICE_MAP = {
"light_flash": "trigger_remote_light_flash",
"sound_horn": "trigger_remote_horn",
"activate_air_conditioning": "trigger_remote_air_conditioning",
}


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the BMW Connected Drive component from configuration.yaml."""
if DOMAIN in config:
for entry_config in list(config[DOMAIN].values()):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": "import"}, data=entry_config
)
)

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up BMW Connected Drive from a config entry."""
hass.data.setdefault(DOMAIN, {})

try:
account = await hass.async_add_executor_job(
setup_account, entry.data, hass, entry.data[CONF_USERNAME]
)
await hass.async_add_executor_job(account.update)
except Exception:
raise ConfigEntryNotReady

hass.data[DOMAIN][entry.entry_id] = account

async def _async_update_all():
"""Update all BMW accounts."""
await hass.async_add_executor_job(_update_all)

return True

def _update_all() -> None:
"""Update all BMW accounts."""
for cd_account in list(hass.data[DOMAIN].values()):
cd_account.update()

# Service to manually trigger updates for all accounts.
hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, _update_all)

await _async_update_all()

# for component in BMW_COMPONENTS:
# await discovery.async_load_platform(hass, component, DOMAIN, {}, entry.data)

for platform in BMW_COMPONENTS:
if platform != "notify":
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)

hass.async_create_task(
discovery.async_load_platform(hass, "notify", DOMAIN, {}, entry.data)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in BMW_COMPONENTS
]
)
)

services = list(_SERVICE_MAP) + [SERVICE_UPDATE_STATE]
unload_services = all(
await asyncio.gather(
*[
hass.async_add_executor_job(hass.services.remove, DOMAIN, service)
for service in services
]
)
)

if all([unload_ok, unload_services]):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok


def setup_account(account_config: dict, hass, name: str) -> "BMWConnectedDriveAccount":
"""Set up a new BMWConnectedDriveAccount based on the config."""
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
region = account_config[CONF_REGION]
read_only = account_config[CONF_READ_ONLY]

_LOGGER.debug("Adding new account %s", name)
cd_account = BMWConnectedDriveAccount(username, password, region, name, read_only)

def execute_service(call):
"""Execute a service for a vehicle.
This must be a member function as we need access to the cd_account
object here.
"""
vin = call.data[ATTR_VIN]
vehicle = cd_account.account.get_vehicle(vin)
if not vehicle:
_LOGGER.error("Could not find a vehicle for VIN %s", vin)
return
function_name = _SERVICE_MAP[call.service]
function_call = getattr(vehicle.remote_services, function_name)
function_call()

if not read_only:
# register the remote services
for service in _SERVICE_MAP:
hass.services.register(
DOMAIN, service, execute_service, schema=SERVICE_SCHEMA
)

# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
now = dt_util.utcnow()
track_utc_time_change(
hass,
cd_account.update,
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
second=now.second,
)

return cd_account


class BMWConnectedDriveAccount:
"""Representation of a BMW vehicle."""

def __init__(
self, username: str, password: str, region_str: str, name: str, read_only
) -> None:
"""Initialize account."""
region = get_region_from_name(region_str)

self.read_only = read_only
self.account = ConnectedDriveAccount(username, password, region)
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 OSError as exception:
_LOGGER.error(
"Could not connect to the BMW Connected Drive portal. "
"The vehicle state could not be updated."
)
_LOGGER.exception(exception)

def add_update_listener(self, listener):
"""Add a listener for update notifications."""
self._update_listeners.append(listener)
Loading

0 comments on commit f309ef0

Please sign in to comment.