diff --git a/.coveragerc b/.coveragerc index 84aed9dfb14..2cfac103f3c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py index f13076fb763..d845d585765 100644 --- a/homeassistant/components/mopar/__init__.py +++ b/homeassistant/components/mopar/__init__.py @@ -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 diff --git a/homeassistant/components/mopar/lock.py b/homeassistant/components/mopar/lock.py new file mode 100644 index 00000000000..aa2e0161813 --- /dev/null +++ b/homeassistant/components/mopar/lock.py @@ -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 diff --git a/homeassistant/components/mopar/sensor.py b/homeassistant/components/mopar/sensor.py index e2dda136244..0d6e5765fda 100644 --- a/homeassistant/components/mopar/sensor.py +++ b/homeassistant/components/mopar/sensor.py @@ -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) diff --git a/homeassistant/components/mopar/services.yaml b/homeassistant/components/mopar/services.yaml new file mode 100644 index 00000000000..7915aefcb0f --- /dev/null +++ b/homeassistant/components/mopar/services.yaml @@ -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 \ No newline at end of file diff --git a/homeassistant/components/mopar/switch.py b/homeassistant/components/mopar/switch.py new file mode 100644 index 00000000000..352cdafbd41 --- /dev/null +++ b/homeassistant/components/mopar/switch.py @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 9dfac8a2130..664e725db61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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