From 20c5f9de4be838f3c0e69911c5c6ebaa817a2927 Mon Sep 17 00:00:00 2001 From: Dan Ports Date: Thu, 23 Mar 2017 11:15:52 -0400 Subject: [PATCH] Add sensor for Lyft time and price (based on Uber sensor) (#6711) * Add sensor for Lyft time and price (based on Uber sensor) * Minor fixes to lyft sensor - use add_devices(...,True) instead of explicitly calling update - move sensor name check into constructor * lyft sensor: disable sandbox mode --- .coveragerc | 1 + homeassistant/components/sensor/lyft.py | 234 ++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 238 insertions(+) create mode 100644 homeassistant/components/sensor/lyft.py diff --git a/.coveragerc b/.coveragerc index 348f7486541..d383cd06b03 100644 --- a/.coveragerc +++ b/.coveragerc @@ -358,6 +358,7 @@ omit = homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/loopenergy.py + homeassistant/components/sensor/lyft.py homeassistant/components/sensor/miflora.py homeassistant/components/sensor/modem_callerid.py homeassistant/components/sensor/mqtt_room.py diff --git a/homeassistant/components/sensor/lyft.py b/homeassistant/components/sensor/lyft.py new file mode 100644 index 00000000000..3962be0aa2f --- /dev/null +++ b/homeassistant/components/sensor/lyft.py @@ -0,0 +1,234 @@ +""" +Support for the Lyft API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.lyft/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['lyft_rides==0.1.0b0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' +CONF_END_LATITUDE = 'end_latitude' +CONF_END_LONGITUDE = 'end_longitude' +CONF_PRODUCT_IDS = 'product_ids' +CONF_START_LATITUDE = 'start_latitude' +CONF_START_LONGITUDE = 'start_longitude' + +ICON = 'mdi:taxi' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Required(CONF_START_LATITUDE): cv.latitude, + vol.Required(CONF_START_LONGITUDE): cv.longitude, + vol.Optional(CONF_END_LATITUDE): cv.latitude, + vol.Optional(CONF_END_LONGITUDE): cv.longitude, + vol.Optional(CONF_PRODUCT_IDS, default=None): + vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Lyft sensor.""" + from lyft_rides.auth import ClientCredentialGrant + + auth_flow = ClientCredentialGrant(client_id=config.get(CONF_CLIENT_ID), + client_secret=config.get( + CONF_CLIENT_SECRET), + scopes="public", + is_sandbox_mode=False) + session = auth_flow.get_session() + + wanted_product_ids = config.get(CONF_PRODUCT_IDS) + + dev = [] + timeandpriceest = LyftEstimate( + session, config[CONF_START_LATITUDE], config[CONF_START_LONGITUDE], + config.get(CONF_END_LATITUDE), config.get(CONF_END_LONGITUDE)) + for product_id, product in timeandpriceest.products.items(): + if (wanted_product_ids is not None) and \ + (product_id not in wanted_product_ids): + continue + dev.append(LyftSensor('time', timeandpriceest, product_id, product)) + if product.get('estimate') is not None: + dev.append(LyftSensor( + 'price', timeandpriceest, product_id, product)) + add_devices(dev, True) + + +class LyftSensor(Entity): + """Implementation of an Lyft sensor.""" + + def __init__(self, sensorType, products, product_id, product): + """Initialize the Lyft sensor.""" + self.data = products + self._product_id = product_id + self._product = product + self._sensortype = sensorType + self._name = '{} {}'.format(self._product['display_name'], + self._sensortype) + if 'lyft' not in self._name.lower(): + self._name = 'Lyft{}'.format(self._name) + if self._sensortype == 'time': + self._unit_of_measurement = 'min' + elif self._sensortype == 'price': + estimate = self._product['estimate'] + if estimate is not None: + self._unit_of_measurement = estimate.get('currency') + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + params = { + 'Product ID': self._product['ride_type'], + 'Product display name': self._product['display_name'], + 'Vehicle Capacity': self._product['seats'] + } + + if self._product.get('pricing_details') is not None: + pricing_details = self._product['pricing_details'] + params['Base price'] = pricing_details.get('base_charge') + params['Cancellation fee'] = pricing_details.get( + 'cancel_penalty_amount') + params['Minimum price'] = pricing_details.get('cost_minimum') + params['Cost per mile'] = pricing_details.get('cost_per_mile') + params['Cost per minute'] = pricing_details.get('cost_per_minute') + params['Price currency code'] = pricing_details.get('currency') + params['Service fee'] = pricing_details.get('trust_and_service') + + if self._product.get("estimate") is not None: + estimate = self._product['estimate'] + params['Trip distance (in miles)'] = estimate.get( + 'estimated_distance_miles') + params['High price estimate (in cents)'] = estimate.get( + 'estimated_cost_cents_max') + params['Low price estimate (in cents)'] = estimate.get( + 'estimated_cost_cents_min') + params['Trip duration (in seconds)'] = estimate.get( + 'estimated_duration_seconds') + + # Ignore the Prime Time percentage -- the Lyft API always + # returns 0 unless a user is logged in. + # params['Prime Time percentage'] = estimate.get( + # 'primetime_percentage') + + if self._product.get("eta") is not None: + eta = self._product['eta'] + params['Pickup time estimate (in seconds)'] = eta.get( + 'eta_seconds') + + return {k: v for k, v in params.items() if v is not None} + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + def update(self): + """Get the latest data from the Lyft API and update the states.""" + self.data.update() + try: + self._product = self.data.products[self._product_id] + except KeyError: + return + if self._sensortype == 'time': + eta = self._product['eta'] + if (eta is not None) and (eta.get('is_valid_estimate')): + time_estimate = eta.get('eta_seconds', 0) + self._state = int(time_estimate / 60) + else: + self._state = 0 + elif self._sensortype == 'price': + estimate = self._product['estimate'] + if (estimate is not None) and \ + estimate.get('is_valid_estimate'): + self._state = (int( + (estimate.get('estimated_cost_cents_min', 0) + + estimate.get('estimated_cost_cents_max', 0)) / 2) / 100) + else: + self._state = 0 + + +class LyftEstimate(object): + """The class for handling the time and price estimate.""" + + def __init__(self, session, start_latitude, start_longitude, + end_latitude=None, end_longitude=None): + """Initialize the LyftEstimate object.""" + self._session = session + self.start_latitude = start_latitude + self.start_longitude = start_longitude + self.end_latitude = end_latitude + self.end_longitude = end_longitude + self.products = None + self.__real_update() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest product info and estimates from the Lyft API.""" + self.__real_update() + + def __real_update(self): + from lyft_rides.client import LyftRidesClient + client = LyftRidesClient(self._session) + + self.products = {} + + products_response = client.get_ride_types( + self.start_latitude, self.start_longitude) + + products = products_response.json.get('ride_types') + + for product in products: + self.products[product['ride_type']] = product + + if self.end_latitude is not None and self.end_longitude is not None: + price_response = client.get_cost_estimates( + self.start_latitude, self.start_longitude, + self.end_latitude, self.end_longitude) + + prices = price_response.json.get('cost_estimates', []) + + for price in prices: + product = self.products[price['ride_type']] + if price.get("is_valid_estimate"): + product['estimate'] = price + + eta_response = client.get_pickup_time_estimates( + self.start_latitude, self.start_longitude) + + etas = eta_response.json.get('eta_estimates') + + for eta in etas: + if eta.get("is_valid_estimate"): + self.products[eta['ride_type']]['eta'] = eta diff --git a/requirements_all.txt b/requirements_all.txt index 8527b9000e2..e766ed0c854 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -353,6 +353,9 @@ limitlessled==1.0.5 # homeassistant.components.media_player.liveboxplaytv liveboxplaytv==1.4.9 +# homeassistant.components.sensor.lyft +lyft_rides==0.1.0b0 + # homeassistant.components.notify.matrix matrix-client==0.0.5