Mopar split (#21526)

* Split out mopar into a component and sensor platform

* Add the mopar switch platform

* Add the mopar lock platform

* Clean up and bump version

* Update per review

* Re-add service to trigger horn

* Clean up again

* Don't call async from sync context

* Lint

* Implement changes from review

* Lint

* A little more clean up
This commit is contained in:
Rohan Kapoor 2019-03-27 20:09:36 -07:00 committed by Paulus Schoutsen
parent f11f5255ae
commit c4eab21736
7 changed files with 317 additions and 115 deletions

View file

@ -328,6 +328,7 @@ omit =
homeassistant/components/mobile_app/*
homeassistant/components/mochad/*
homeassistant/components/modbus/*
homeassistant/components/mopar/*
homeassistant/components/mychevy/*
homeassistant/components/mycroft/*
homeassistant/components/mysensors/*
@ -499,7 +500,7 @@ omit =
homeassistant/components/miflora/sensor.py
homeassistant/components/mitemp_bt/sensor.py
homeassistant/components/modem_callerid/sensor.py
homeassistant/components/mopar/sensor.py
homeassistant/components/mopar/*
homeassistant/components/mqtt_room/sensor.py
homeassistant/components/mvglive/sensor.py
homeassistant/components/nederlandse_spoorwegen/sensor.py

View file

@ -1 +1,157 @@
"""The mopar component."""
"""Support for Mopar vehicles."""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.lock import DOMAIN as LOCK
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.const import (
CONF_USERNAME,
CONF_PASSWORD,
CONF_PIN,
CONF_SCAN_INTERVAL
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import track_time_interval
REQUIREMENTS = ['motorparts==1.1.0']
DOMAIN = 'mopar'
DATA_UPDATED = '{}_data_updated'.format(DOMAIN)
_LOGGER = logging.getLogger(__name__)
COOKIE_FILE = 'mopar_cookies.pickle'
SUCCESS_RESPONSE = 'completed'
SUPPORTED_PLATFORMS = [LOCK, SENSOR, SWITCH]
DEFAULT_INTERVAL = timedelta(days=7)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_PIN): cv.positive_int,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL):
vol.All(cv.time_period, cv.positive_timedelta),
})
}, extra=vol.ALLOW_EXTRA)
SERVICE_HORN = 'sound_horn'
ATTR_VEHICLE_INDEX = 'vehicle_index'
SERVICE_HORN_SCHEMA = vol.Schema({
vol.Required(ATTR_VEHICLE_INDEX): cv.positive_int
})
def setup(hass, config):
"""Set up the Mopar component."""
import motorparts
cookie = hass.config.path(COOKIE_FILE)
try:
session = motorparts.get_session(
config[CONF_USERNAME],
config[CONF_PASSWORD],
config[CONF_PIN],
cookie_path=cookie
)
except motorparts.MoparError:
_LOGGER.error("Failed to login")
return False
data = hass.data[DOMAIN] = MoparData(hass, session)
data.update(now=None)
track_time_interval(
hass, data.update, config[CONF_SCAN_INTERVAL]
)
def handle_horn(call):
"""Enable the horn on the Mopar vehicle."""
data.actuate('horn', call.data[ATTR_VEHICLE_INDEX])
hass.services.register(
DOMAIN,
SERVICE_HORN,
handle_horn,
schema=SERVICE_HORN_SCHEMA
)
for platform in SUPPORTED_PLATFORMS:
load_platform(hass, platform, DOMAIN, {}, config)
return True
class MoparData:
"""
Container for Mopar vehicle data.
Prevents session expiry re-login race condition.
"""
def __init__(self, hass, session):
"""Initialize data."""
self._hass = hass
self._session = session
self.vehicles = []
self.vhrs = {}
self.tow_guides = {}
def update(self, now, **kwargs):
"""Update data."""
import motorparts
_LOGGER.debug("Updating vehicle data")
try:
self.vehicles = motorparts.get_summary(self._session)['vehicles']
except motorparts.MoparError:
_LOGGER.exception("Failed to get summary")
return
for index, _ in enumerate(self.vehicles):
try:
self.vhrs[index] = motorparts.get_report(self._session, index)
self.tow_guides[index] = motorparts.get_tow_guide(
self._session, index)
except motorparts.MoparError:
_LOGGER.warning("Failed to update for vehicle index %s", index)
return
dispatcher_send(self._hass, DATA_UPDATED)
@property
def attribution(self):
"""Get the attribution string from Mopar."""
import motorparts
return motorparts.ATTRIBUTION
def get_vehicle_name(self, index):
"""Get the name corresponding with this vehicle."""
vehicle = self.vehicles[index]
if not vehicle:
return None
return '{} {} {}'.format(
vehicle['year'],
vehicle['make'],
vehicle['model']
)
def actuate(self, command, index):
"""Run a command on the specified Mopar vehicle."""
import motorparts
try:
response = getattr(motorparts, command)(self._session, index)
except motorparts.MoparError as error:
_LOGGER.error(error)
return False
return response == SUCCESS_RESPONSE

View file

@ -0,0 +1,55 @@
"""Support for the Mopar vehicle lock."""
import logging
from homeassistant.components.lock import LockDevice
from homeassistant.components.mopar import (
DOMAIN as MOPAR_DOMAIN
)
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
DEPENDENCIES = ['mopar']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Mopar lock platform."""
data = hass.data[MOPAR_DOMAIN]
add_entities([MoparLock(data, index)
for index, _ in enumerate(data.vehicles)], True)
class MoparLock(LockDevice):
"""Representation of a Mopar vehicle lock."""
def __init__(self, data, index):
"""Initialize the Mopar lock."""
self._index = index
self._name = '{} Lock'.format(data.get_vehicle_name(self._index))
self._actuate = data.actuate
self._state = None
@property
def name(self):
"""Return the name of the lock."""
return self._name
@property
def is_locked(self):
"""Return true if vehicle is locked."""
return self._state == STATE_LOCKED
@property
def should_poll(self):
"""Return the polling requirement for this lock."""
return False
def lock(self, **kwargs):
"""Lock the vehicle."""
if self._actuate('lock', self._index):
self._state = STATE_LOCKED
def unlock(self, **kwargs):
"""Unlock the vehicle."""
if self._actuate('unlock', self._index):
self._state = STATE_UNLOCKED

View file

@ -1,108 +1,27 @@
"""
Sensor for Mopar vehicles.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mopar/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA
"""Support for the Mopar vehicle sensor platform."""
from homeassistant.components.mopar import (
DOMAIN as MOPAR_DOMAIN,
DATA_UPDATED,
ATTR_VEHICLE_INDEX
)
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_COMMAND, CONF_PASSWORD, CONF_PIN, CONF_USERNAME,
LENGTH_KILOMETERS)
import homeassistant.helpers.config_validation as cv
ATTR_ATTRIBUTION, LENGTH_KILOMETERS)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['motorparts==1.1.0']
_LOGGER = logging.getLogger(__name__)
ATTR_VEHICLE_INDEX = 'vehicle_index'
COOKIE_FILE = 'mopar_cookies.pickle'
MIN_TIME_BETWEEN_UPDATES = timedelta(days=7)
SERVICE_REMOTE_COMMAND = 'mopar_remote_command'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_PIN): cv.positive_int,
})
REMOTE_COMMAND_SCHEMA = vol.Schema({
vol.Required(ATTR_COMMAND): cv.string,
vol.Required(ATTR_VEHICLE_INDEX): cv.positive_int
})
DEPENDENCIES = ['mopar']
ICON = 'mdi:car'
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, add_entities,
discovery_info=None):
"""Set up the Mopar platform."""
import motorparts
cookie = hass.config.path(COOKIE_FILE)
try:
session = motorparts.get_session(
config.get(CONF_USERNAME), config.get(CONF_PASSWORD),
config.get(CONF_PIN), cookie_path=cookie)
except motorparts.MoparError:
_LOGGER.error("Failed to login")
return
def _handle_service(service):
"""Handle service call."""
index = service.data.get(ATTR_VEHICLE_INDEX)
command = service.data.get(ATTR_COMMAND)
try:
motorparts.remote_command(session, command, index)
except motorparts.MoparError as error:
_LOGGER.error(str(error))
hass.services.register(DOMAIN, SERVICE_REMOTE_COMMAND, _handle_service,
schema=REMOTE_COMMAND_SCHEMA)
data = MoparData(session)
data = hass.data[MOPAR_DOMAIN]
add_entities([MoparSensor(data, index)
for index, _ in enumerate(data.vehicles)], True)
class MoparData:
"""Container for Mopar vehicle data.
Prevents session expiry re-login race condition.
"""
def __init__(self, session):
"""Initialize data."""
self._session = session
self.vehicles = []
self.vhrs = {}
self.tow_guides = {}
self.update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs):
"""Update data."""
import motorparts
_LOGGER.info("Updating vehicle data")
try:
self.vehicles = motorparts.get_summary(self._session)['vehicles']
except motorparts.MoparError:
_LOGGER.exception("Failed to get summary")
return
for index, _ in enumerate(self.vehicles):
try:
self.vhrs[index] = motorparts.get_report(self._session, index)
self.tow_guides[index] = motorparts.get_tow_guide(
self._session, index)
except motorparts.MoparError:
_LOGGER.warning("Failed to update for vehicle index %s", index)
class MoparSensor(Entity):
"""Mopar vehicle sensor."""
@ -114,24 +33,12 @@ class MoparSensor(Entity):
self._tow_guide = {}
self._odometer = None
self._data = data
def update(self):
"""Update device state."""
self._data.update()
self._vehicle = self._data.vehicles[self._index]
self._vhr = self._data.vhrs.get(self._index, {})
self._tow_guide = self._data.tow_guides.get(self._index, {})
if 'odometer' in self._vhr:
odo = float(self._vhr['odometer'])
self._odometer = int(self.hass.config.units.length(
odo, LENGTH_KILOMETERS))
self._name = self._data.get_vehicle_name(self._index)
@property
def name(self):
"""Return the name of the sensor."""
return '{} {} {}'.format(
self._vehicle['year'], self._vehicle['make'],
self._vehicle['model'])
return self._name
@property
def state(self):
@ -141,10 +48,9 @@ class MoparSensor(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
import motorparts
attributes = {
ATTR_VEHICLE_INDEX: self._index,
ATTR_ATTRIBUTION: motorparts.ATTRIBUTION
ATTR_ATTRIBUTION: self._data.attribution
}
attributes.update(self._vehicle)
attributes.update(self._vhr)
@ -159,4 +65,29 @@ class MoparSensor(Entity):
@property
def icon(self):
"""Return the icon."""
return 'mdi:car'
return ICON
@property
def should_poll(self):
"""Return the polling requirement for this sensor."""
return False
async def async_added_to_hass(self):
"""Handle entity which will be added."""
async_dispatcher_connect(
self.hass, DATA_UPDATED, self._schedule_immediate_update
)
def update(self):
"""Update device state."""
self._vehicle = self._data.vehicles[self._index]
self._vhr = self._data.vhrs.get(self._index, {})
self._tow_guide = self._data.tow_guides.get(self._index, {})
if 'odometer' in self._vhr:
odo = float(self._vhr['odometer'])
self._odometer = int(self.hass.config.units.length(
odo, LENGTH_KILOMETERS))
@callback
def _schedule_immediate_update(self):
self.async_schedule_update_ha_state(True)

View file

@ -0,0 +1,6 @@
sound_horn:
description: Trigger the vehicle's horn
fields:
vehicle_index:
description: The index of the vehicle to trigger. This is exposed in the sensor's device attributes.
example: 1

View file

@ -0,0 +1,53 @@
"""Support for the Mopar vehicle switch."""
import logging
from homeassistant.components.mopar import DOMAIN as MOPAR_DOMAIN
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import STATE_ON, STATE_OFF
DEPENDENCIES = ['mopar']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Mopar Switch platform."""
data = hass.data[MOPAR_DOMAIN]
add_entities([MoparSwitch(data, index)
for index, _ in enumerate(data.vehicles)], True)
class MoparSwitch(SwitchDevice):
"""Representation of a Mopar switch."""
def __init__(self, data, index):
"""Initialize the Switch."""
self._index = index
self._name = '{} Switch'.format(data.get_vehicle_name(self._index))
self._actuate = data.actuate
self._state = None
@property
def name(self):
"""Return the name of the switch."""
return self._name
@property
def is_on(self):
"""Return True if the entity is on."""
return self._state == STATE_ON
@property
def should_poll(self):
"""Return the polling requirement for this switch."""
return False
def turn_on(self, **kwargs):
"""Turn on the Mopar Vehicle."""
if self._actuate('engine_on', self._index):
self._state = STATE_ON
def turn_off(self, **kwargs):
"""Turn off the Mopar Vehicle."""
if self._actuate('engine_off', self._index):
self._state = STATE_OFF

View file

@ -709,7 +709,7 @@ millheater==0.3.4
# homeassistant.components.mitemp_bt.sensor
mitemp_bt==0.0.1
# homeassistant.components.mopar.sensor
# homeassistant.components.mopar
motorparts==1.1.0
# homeassistant.components.tts