diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index f5a8217242d..cb101c0a530 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -1,13 +1,13 @@ """Support for the Nissan Leaf Carwings/Nissan Connect API.""" -import asyncio from datetime import datetime, timedelta +import asyncio import logging import sys -import urllib import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import ( @@ -78,32 +78,60 @@ LEAF_COMPONENTS = [ SIGNAL_UPDATE_LEAF = 'nissan_leaf_update' SERVICE_UPDATE_LEAF = 'update' +SERVICE_START_CHARGE_LEAF = 'start_charge' ATTR_VIN = 'vin' UPDATE_LEAF_SCHEMA = vol.Schema({ vol.Required(ATTR_VIN): cv.string, }) +START_CHARGE_LEAF_SCHEMA = vol.Schema({ + vol.Required(ATTR_VIN): cv.string, +}) -async def async_setup(hass, config): +def setup(hass, config): """Set up the Nissan Leaf component.""" import pycarwings2 - async def handle_update(service): + async def async_handle_update(service): + """Handle service to update leaf data from Nissan servers.""" # It would be better if this was changed to use nickname, or # an entity name rather than a vin. - vin = service.data.get(ATTR_VIN, '') + vin = service.data[ATTR_VIN] if vin in hass.data[DATA_LEAF]: data_store = hass.data[DATA_LEAF][vin] - async_track_point_in_utc_time( - hass, data_store.async_update_data, utcnow()) - return True + await data_store.async_update_data(utcnow()) + else: + _LOGGER.debug("Vin %s not recognised for update", vin) - _LOGGER.debug("Vin %s not recognised for update", vin) - return False + async def async_handle_start_charge(service): + """Handle service to start charging.""" + # It would be better if this was changed to use nickname, or + # an entity name rather than a vin. + vin = service.data[ATTR_VIN] - async def async_setup_leaf(car_config): + if vin in hass.data[DATA_LEAF]: + data_store = hass.data[DATA_LEAF][vin] + + # Send the command to request charging is started to Nissan + # servers. If that completes OK then trigger a fresh update to + # pull the charging status from the car after waiting a minute + # for the charging request to reach the car. + result = await hass.async_add_executor_job( + data_store.leaf.start_charging) + if result: + _LOGGER.debug("Start charging sent, " + "request updated data in 1 minute") + check_charge_at = utcnow() + timedelta(minutes=1) + data_store.next_update = check_charge_at + async_track_point_in_utc_time( + hass, data_store.async_update_data, check_charge_at) + + else: + _LOGGER.debug("Vin %s not recognised for update", vin) + + def setup_leaf(car_config): """Set up a car.""" _LOGGER.debug("Logging into You+Nissan...") @@ -112,20 +140,11 @@ async def async_setup(hass, config): region = car_config[CONF_REGION] leaf = None - async def leaf_login(): - nonlocal leaf - sess = pycarwings2.Session(username, password, region) - leaf = sess.get_leaf() - try: # This might need to be made async (somehow) causes # homeassistant to be slow to start - await hass.async_add_job(leaf_login) - except(RuntimeError, urllib.error.HTTPError): - _LOGGER.error( - "Unable to connect to Nissan Connect with " - "username and password") - return False + sess = pycarwings2.Session(username, password, region) + leaf = sess.get_leaf() except KeyError: _LOGGER.error( "Unable to fetch car details..." @@ -143,7 +162,7 @@ async def async_setup(hass, config): " as the drive train battery won't connect." " Don't set the intervals too low.") - data_store = LeafDataStore(leaf, hass, car_config) + data_store = LeafDataStore(hass, leaf, car_config) hass.data[DATA_LEAF][leaf.vin] = data_store for component in LEAF_COMPONENTS: @@ -154,12 +173,15 @@ async def async_setup(hass, config): utcnow() + INITIAL_UPDATE) hass.data[DATA_LEAF] = {} - tasks = [async_setup_leaf(car) for car in config[DOMAIN]] - if tasks: - await asyncio.wait(tasks, loop=hass.loop) + for car in config[DOMAIN]: + setup_leaf(car) - hass.services.async_register(DOMAIN, SERVICE_UPDATE_LEAF, handle_update, - schema=UPDATE_LEAF_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_UPDATE_LEAF, + async_handle_update, schema=UPDATE_LEAF_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_START_CHARGE_LEAF, + async_handle_start_charge, schema=START_CHARGE_LEAF_SCHEMA) return True @@ -167,13 +189,13 @@ async def async_setup(hass, config): class LeafDataStore: """Nissan Leaf Data Store.""" - def __init__(self, leaf, hass, car_config): + def __init__(self, hass, leaf, car_config): """Initialise the data store.""" + self.hass = hass self.leaf = leaf self.car_config = car_config self.nissan_connect = car_config[CONF_NCONNECT] self.force_miles = car_config[CONF_FORCE_MILES] - self.hass = hass self.data = {} self.data[DATA_CLIMATE] = False self.data[DATA_BATTERY] = 0 @@ -223,25 +245,19 @@ class LeafDataStore: if (self.last_battery_response is not None and self.data[DATA_CHARGING] is False and self.data[DATA_BATTERY] <= RESTRICTED_BATTERY): - _LOGGER.info("Low battery so restricting refresh frequency (%s)", - self.leaf.nickname) + _LOGGER.debug("Low battery so restricting refresh frequency (%s)", + self.leaf.nickname) interval = RESTRICTED_INTERVAL else: intervals = [base_interval] - _LOGGER.debug("Could use base interval=%s", base_interval) if self.data[DATA_CHARGING]: intervals.append(charging_interval) - _LOGGER.debug("Could use charging interval=%s", - charging_interval) if self.data[DATA_CLIMATE]: intervals.append(climate_interval) - _LOGGER.debug( - "Could use climate interval=%s", climate_interval) interval = min(intervals) - _LOGGER.debug("Resulting interval=%s", interval) return utcnow() + interval @@ -310,12 +326,10 @@ class LeafDataStore: _LOGGER.debug("Empty Location Response Received") self.data[DATA_LOCATION] = None else: - _LOGGER.debug("Got location data for Leaf") - self.data[DATA_LOCATION] = location_response - self.last_location_response = utcnow() - _LOGGER.debug("Location Response: %s", location_response.__dict__) + self.data[DATA_LOCATION] = location_response + self.last_location_response = utcnow() except CarwingsError: _LOGGER.error("Error fetching location info") @@ -336,9 +350,8 @@ class LeafDataStore: from pycarwings2 import CarwingsError try: # First, check nissan servers for the latest data - start_server_info = await self.hass.async_add_job( - self.leaf.get_latest_battery_status - ) + start_server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status) # Store the date from the nissan servers start_date = self._extract_start_date(start_server_info) @@ -346,33 +359,35 @@ class LeafDataStore: _LOGGER.info("No start date from servers. Aborting") return None - _LOGGER.info("Start server date=%s", start_date) + _LOGGER.debug("Start server date=%s", start_date) # Request battery update from the car - _LOGGER.info("Requesting battery update, %s", self.leaf.vin) - request = await self.hass.async_add_job(self.leaf.request_update) + _LOGGER.debug("Requesting battery update, %s", self.leaf.vin) + request = await self.hass.async_add_executor_job( + self.leaf.request_update) if not request: _LOGGER.error("Battery update request failed") return None for attempt in range(MAX_RESPONSE_ATTEMPTS): - _LOGGER.info("Waiting %s seconds for battery update (%s) (%s)", - PYCARWINGS2_SLEEP, self.leaf.vin, attempt) + _LOGGER.debug( + "Waiting %s seconds for battery update (%s) (%s)", + PYCARWINGS2_SLEEP, self.leaf.vin, attempt) await asyncio.sleep(PYCARWINGS2_SLEEP) # Note leaf.get_status_from_update is always returning 0, so # don't try to use it anymore. - server_info = await self.hass.async_add_job( - self.leaf.get_latest_battery_status - ) + server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status) latest_date = self._extract_start_date(server_info) - _LOGGER.info("Latest server date=%s", latest_date) + _LOGGER.debug("Latest server date=%s", latest_date) if latest_date is not None and latest_date != start_date: return server_info - _LOGGER.info("%s attempts exceeded return latest data from server", - MAX_RESPONSE_ATTEMPTS) + _LOGGER.debug( + "%s attempts exceeded return latest data from server", + MAX_RESPONSE_ATTEMPTS) return server_info except CarwingsError: _LOGGER.error("An error occurred getting battery status.") @@ -382,10 +397,8 @@ class LeafDataStore: """Request climate data from Nissan servers.""" from pycarwings2 import CarwingsError try: - request = await self.hass.async_add_job( - self.leaf.get_latest_hvac_status - ) - return request + return await self.hass.async_add_executor_job( + self.leaf.get_latest_hvac_status) except CarwingsError: _LOGGER.error( "An error occurred communicating with the car %s", @@ -396,43 +409,27 @@ class LeafDataStore: """Set climate control mode via Nissan servers.""" climate_result = None if toggle: - _LOGGER.info("Requesting climate turn on for %s", self.leaf.vin) - request = await self.hass.async_add_job( - self.leaf.start_climate_control - ) - for attempt in range(MAX_RESPONSE_ATTEMPTS): - if attempt > 0: - _LOGGER.info("Climate data not in yet (%s) (%s). " - "Waiting (%s) seconds.", self.leaf.vin, - attempt, PYCARWINGS2_SLEEP) - await asyncio.sleep(PYCARWINGS2_SLEEP) - - climate_result = await self.hass.async_add_job( - self.leaf.get_start_climate_control_result, request - ) - - if climate_result is not None: - break - + _LOGGER.debug("Requesting climate turn on for %s", self.leaf.vin) + set_function = self.leaf.start_climate_control + result_function = self.leaf.get_start_climate_control_result else: - _LOGGER.info("Requesting climate turn off for %s", self.leaf.vin) - request = await self.hass.async_add_job( - self.leaf.stop_climate_control - ) + _LOGGER.debug("Requesting climate turn off for %s", self.leaf.vin) + set_function = self.leaf.stop_climate_control + result_function = self.leaf.get_stop_climate_control_result - for attempt in range(MAX_RESPONSE_ATTEMPTS): - if attempt > 0: - _LOGGER.debug("Climate data not in yet. (%s) (%s). " - "Waiting %s seconds", self.leaf.vin, - attempt, PYCARWINGS2_SLEEP) - await asyncio.sleep(PYCARWINGS2_SLEEP) + request = await self.hass.async_add_executor_job(set_function) + for attempt in range(MAX_RESPONSE_ATTEMPTS): + if attempt > 0: + _LOGGER.debug("Climate data not in yet (%s) (%s). " + "Waiting (%s) seconds", self.leaf.vin, + attempt, PYCARWINGS2_SLEEP) + await asyncio.sleep(PYCARWINGS2_SLEEP) - climate_result = await self.hass.async_add_job( - self.leaf.get_stop_climate_control_result, request - ) + climate_result = await self.hass.async_add_executor_job( + result_function, request) - if climate_result is not None: - break + if climate_result is not None: + break if climate_result is not None: _LOGGER.debug("Climate result: %s", climate_result.__dict__) @@ -444,7 +441,8 @@ class LeafDataStore: async def async_get_location(self): """Get location from Nissan servers.""" - request = await self.hass.async_add_job(self.leaf.request_location) + request = await self.hass.async_add_executor_job( + self.leaf.request_location) for attempt in range(MAX_RESPONSE_ATTEMPTS): if attempt > 0: _LOGGER.debug("Location data not in yet. (%s) (%s). " @@ -452,9 +450,8 @@ class LeafDataStore: attempt, PYCARWINGS2_SLEEP) await asyncio.sleep(PYCARWINGS2_SLEEP) - location_status = await self.hass.async_add_job( - self.leaf.get_status_from_location, request - ) + location_status = await self.hass.async_add_executor_job( + self.leaf.get_status_from_location, request) if location_status is not None: _LOGGER.debug("Location_status=%s", location_status.__dict__) @@ -462,21 +459,6 @@ class LeafDataStore: return location_status - async def async_start_charging(self): - """Request start charging via Nissan servers.""" - # Send the command to request charging is started to Nissan servers. - # If that completes OK then trigger a fresh update to pull the - # charging status from the car after waiting a minute for the - # charging request to reach the car. - result = await self.hass.async_add_job(self.leaf.start_charging) - if result: - _LOGGER.debug("Start charging sent, " - "request updated data in 1 minute") - check_charge_at = utcnow() + timedelta(minutes=1) - self.next_update = check_charge_at - async_track_point_in_utc_time( - self.hass, self.async_update_data, check_charge_at) - class LeafEntity(Entity): """Base class for Nissan Leaf entity.""" @@ -499,7 +481,6 @@ class LeafEntity(Entity): 'last_attempt': self.car.last_check, 'updated_on': self.car.last_battery_response, 'update_in_progress': self.car.request_in_progress, - 'location_updated_on': self.car.last_location_response, 'vin': self.car.leaf.vin, } @@ -509,6 +490,7 @@ class LeafEntity(Entity): async_dispatcher_connect( self.car.hass, SIGNAL_UPDATE_LEAF, self._update_callback) + @callback def _update_callback(self): """Update the state.""" - self.schedule_update_ha_state(True) + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index 05255d616c4..2397405ec20 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -2,28 +2,29 @@ import logging from homeassistant.components.nissan_leaf import ( - DATA_LEAF, DATA_PLUGGED_IN, LeafEntity) + DATA_CHARGING, DATA_LEAF, DATA_PLUGGED_IN, LeafEntity) +from homeassistant.components.binary_sensor import BinarySensorDevice _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['nissan_leaf'] -def setup_platform(hass, config, add_devices, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up of a Nissan Leaf binary sensor.""" - _LOGGER.debug( - "binary_sensor setup_platform, discovery_info=%s", discovery_info) + if discovery_info is None: + return devices = [] - for key, value in hass.data[DATA_LEAF].items(): - _LOGGER.debug( - "binary_sensor setup_platform, key=%s, value=%s", key, value) - devices.append(LeafPluggedInSensor(value)) + for vin, datastore in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding binary_sensors for vin=%s", vin) + devices.append(LeafPluggedInSensor(datastore)) + devices.append(LeafChargingSensor(datastore)) - add_devices(devices, True) + add_entities(devices, True) -class LeafPluggedInSensor(LeafEntity): +class LeafPluggedInSensor(LeafEntity, BinarySensorDevice): """Plugged In Sensor class.""" @property @@ -32,7 +33,7 @@ class LeafPluggedInSensor(LeafEntity): return "{} {}".format(self.car.leaf.nickname, "Plug Status") @property - def state(self): + def is_on(self): """Return true if plugged in.""" return self.car.data[DATA_PLUGGED_IN] @@ -42,3 +43,24 @@ class LeafPluggedInSensor(LeafEntity): if self.car.data[DATA_PLUGGED_IN]: return 'mdi:power-plug' return 'mdi:power-plug-off' + + +class LeafChargingSensor(LeafEntity, BinarySensorDevice): + """Charging Sensor class.""" + + @property + def name(self): + """Sensor name.""" + return "{} {}".format(self.car.leaf.nickname, "Charging Status") + + @property + def is_on(self): + """Return true if charging.""" + return self.car.data[DATA_CHARGING] + + @property + def icon(self): + """Icon handling.""" + if self.car.data[DATA_CHARGING]: + return 'mdi:flash' + return 'mdi:flash-off' diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py index 163675319ea..1ca7fceb911 100644 --- a/homeassistant/components/nissan_leaf/device_tracker.py +++ b/homeassistant/components/nissan_leaf/device_tracker.py @@ -15,28 +15,28 @@ ICON_CAR = "mdi:car" def setup_scanner(hass, config, see, discovery_info=None): """Set up the Nissan Leaf tracker.""" - _LOGGER.debug("Setting up Scanner (device_tracker) for Nissan Leaf, " - "discovery_info=%s", discovery_info) + if discovery_info is None: + return False def see_vehicle(): """Handle the reporting of the vehicle position.""" - for key, value in hass.data[DATA_LEAF].items(): - host_name = value.leaf.nickname + for vin, datastore in hass.data[DATA_LEAF].items(): + host_name = datastore.leaf.nickname dev_id = 'nissan_leaf_{}'.format(slugify(host_name)) - if not value.data[DATA_LOCATION]: - _LOGGER.debug("No position found for vehicle %s", key) - return False + if not datastore.data[DATA_LOCATION]: + _LOGGER.debug("No position found for vehicle %s", vin) + return _LOGGER.debug("Updating device_tracker for %s with position %s", - value.leaf.nickname, - value.data[DATA_LOCATION].__dict__) + datastore.leaf.nickname, + datastore.data[DATA_LOCATION].__dict__) attrs = { - 'updated_on': value.last_location_response, + 'updated_on': datastore.last_location_response, } see(dev_id=dev_id, host_name=host_name, gps=( - value.data[DATA_LOCATION].latitude, - value.data[DATA_LOCATION].longitude + datastore.data[DATA_LOCATION].latitude, + datastore.data[DATA_LOCATION].longitude ), attributes=attrs, icon=ICON_CAR) diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 3c8f9ab9ef3..f6206f1f4ef 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -18,15 +18,15 @@ ICON_RANGE = 'mdi:speedometer' def setup_platform(hass, config, add_devices, discovery_info=None): """Sensors setup.""" - _LOGGER.debug("setup_platform nissan_leaf sensors, discovery_info=%s", - discovery_info) + if discovery_info is None: + return devices = [] - for key, value in hass.data[DATA_LEAF].items(): - _LOGGER.debug("adding sensor for item key=%s, value=%s", key, value) - devices.append(LeafBatterySensor(value)) - devices.append(LeafRangeSensor(value, True)) - devices.append(LeafRangeSensor(value, False)) + for vin, datastore in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding sensors for vin=%s", vin) + devices.append(LeafBatterySensor(datastore)) + devices.append(LeafRangeSensor(datastore, True)) + devices.append(LeafRangeSensor(datastore, False)) add_devices(devices, True) diff --git a/homeassistant/components/nissan_leaf/services.yaml b/homeassistant/components/nissan_leaf/services.yaml new file mode 100644 index 00000000000..ef60dfb4a65 --- /dev/null +++ b/homeassistant/components/nissan_leaf/services.yaml @@ -0,0 +1,21 @@ +# Describes the format for available services for nissan_leaf + +start_charge: + description: > + Start the vehicle charging. It must be plugged in first! + fields: + vin: + description: > + The vehicle identification number (VIN) of the vehicle, 17 characters + example: WBANXXXXXX1234567 + +update: + description: > + Fetch the last state of the vehicle of all your accounts, requesting + an update from of the state from the car if possible. + fields: + vin: + description: > + The vehicle identification number (VIN) of the vehicle, 17 characters + example: WBANXXXXXX1234567 + diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index 914e85b48a6..60b9a6630cd 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.nissan_leaf import ( - DATA_CHARGING, DATA_CLIMATE, DATA_LEAF, LeafEntity) + DATA_CLIMATE, DATA_LEAF, LeafEntity) from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -12,13 +12,13 @@ DEPENDENCIES = ['nissan_leaf'] def setup_platform(hass, config, add_devices, discovery_info=None): """Nissan Leaf switch platform setup.""" - _LOGGER.debug( - "In switch setup platform, discovery_info=%s", discovery_info) + if discovery_info is None: + return devices = [] - for value in hass.data[DATA_LEAF].values(): - devices.append(LeafChargeSwitch(value)) - devices.append(LeafClimateSwitch(value)) + for vin, datastore in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding switch for vin=%s", vin) + devices.append(LeafClimateSwitch(datastore)) add_devices(devices, True) @@ -40,7 +40,7 @@ class LeafClimateSwitch(LeafEntity, ToggleEntity): @property def device_state_attributes(self): """Return climate control attributes.""" - attrs = super(LeafClimateSwitch, self).device_state_attributes + attrs = super().device_state_attributes attrs["updated_on"] = self.car.last_climate_response return attrs @@ -58,42 +58,3 @@ class LeafClimateSwitch(LeafEntity, ToggleEntity): """Turn off climate control.""" if await self.car.async_set_climate(False): self.car.data[DATA_CLIMATE] = False - - @property - def icon(self): - """Climate control icon.""" - if self.car.data[DATA_CLIMATE]: - return 'mdi:fan' - return 'mdi:fan-off' - - -class LeafChargeSwitch(LeafEntity, ToggleEntity): - """Nissan Leaf Charging On switch.""" - - @property - def name(self): - """Switch name.""" - return "{} {}".format(self.car.leaf.nickname, "Charging Status") - - @property - def icon(self): - """Charging switch icon.""" - if self.car.data[DATA_CHARGING]: - return 'mdi:flash' - return 'mdi:flash-off' - - @property - def is_on(self): - """Return true if charging.""" - return self.car.data[DATA_CHARGING] - - async def async_turn_on(self, **kwargs): - """Start car charging.""" - if await self.car.async_start_charging(): - self.car.data[DATA_CHARGING] = True - - def turn_off(self, **kwargs): - """Nissan API doesn't allow stopping of charge remotely.""" - _LOGGER.info( - "Cannot turn off Leaf charging." - " Nissan API does not support stopping charge remotely")