From fb561ddb40b62b00b09951fd23dadb28ee5ac693 Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Thu, 11 Jan 2018 16:40:01 +0000 Subject: [PATCH 01/78] Added work so far. --- homeassistant/components/nissan_leaf.py | 127 ++++++++++++++++++ .../components/sensor/nissan_leaf.py | 42 ++++++ .../components/switch/nissan_leaf.py | 71 ++++++++++ 3 files changed, 240 insertions(+) create mode 100644 homeassistant/components/nissan_leaf.py create mode 100644 homeassistant/components/sensor/nissan_leaf.py create mode 100644 homeassistant/components/switch/nissan_leaf.py diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py new file mode 100644 index 0000000000000..e042b38e2c6c4 --- /dev/null +++ b/homeassistant/components/nissan_leaf.py @@ -0,0 +1,127 @@ +import voluptuous as vol +import logging +from datetime import timedelta +import urllib + +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ['https://github.com/BenWoodford/pycarwings2/archive/master.zip' + '#pycarwings'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'nissan_leaf' +DATA_LEAF = 'nissan_leaf_data' + +DATA_BATTERY = 'battery' +DATA_LOCATION = 'location' +DATA_CHARGING = 'charging' +DATA_CLIMATE = 'climate' + +CONF_INTERVAL = 'update_interval' +DEFAULT_INTERVAL = 1 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int + }) +}, extra=vol.ALLOW_EXTRA) + +LEAF_COMPONENTS = [ + 'sensor', 'switch'#, 'device_tracker', +] + +def setup(hass, config): + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + + import pycarwings2 + + try: + s = pycarwings2.Session(username, password, "NE") + _LOGGER.info("Logging into CarWings/NissanConnect Account") + leaf = s.get_leaf() + except(RuntimeError, urllib.error.HTTPError): + _LOGGER.error("Unable to connect to Nissan Connect with username and password") + return False + except(KeyError): + _LOGGER.error("Unable to fetch car details... do you actually have a Leaf connected to your account?") + return False + + hass.data[DATA_LEAF] = LeafDataStore(leaf) + + for component in LEAF_COMPONENTS: + load_platform(hass, component, DOMAIN, {}, config) + + return True + +class LeafDataStore: + + def __init__(self, leaf): + self.leaf = leaf + self.data = {} + + @Throttle(timedelta(minutes=DEFAULT_INTERVAL)) + def update(self): + batteryResponse = self.get_battery() + if batteryResponse.answer == 200: + self.data[DATA_BATTERY] = 0 + self.data[DATA_CHARGING] = batteryResponse.is_charging + + climateResponse = self.get_climate() + self.data[DATA_CLIMATE] = climateResponse.is_hvac_running + + locationResponse = self.get_location() + self.data[DATA_LOCATION] = locationResponse + + def get_battery(self): + request = self.leaf.request_update() + battery_status = self.leaf.get_status_from_update(request) + while battery_status is None: + time.sleep(5) + battery_status = self.leaf.get_status_from_update(request) + + _LOGGER.info(battery_status) + + return battery_status + + def get_climate(self): + return None + + def set_climate(self, toggle): + if toggle: + request = self.leaf.start_climate_control() + climate_result = self.leaf.get_start_climate_control_result(request) + + while climate_result is None: + time.sleep(5) + climate_result = self.leaf.get_start_climate_control_result(request) + + return climate_result.is_hvac_running + else: + request = self.leaf.stop_climate_control() + climate_result = self.leaf.get_stop_climate_control_result(request) + + while climate_result is None: + time.sleep(5) + climate_result = self.leaf.get_stop_climate_control_result(request) + + return not climate_result.is_hvac_running + + def get_location(self): + request = self.left.request_location() + location_status = self.leaf.get_status_from_location(request) + + while location_status is None: + time.sleep(5) + location_status = self.leaf.get_status_from_location(request) + + return location_status + + def start_charging(self): + return self.leaf.start_charging() \ No newline at end of file diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py new file mode 100644 index 0000000000000..70bbd0e839866 --- /dev/null +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -0,0 +1,42 @@ +""" +Battery Level Sensor for Nissan Leaf +""" + +import logging +from datetime import timedelta + +from homeassistant.components.sensor import ENTITY_ID_FORMAT +import custom_components.leaf as LeafCore +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + +def setup_platform(hass, config, add_devices, discovery_info=None): + controller = hass.data[LeafCore.DATA_LEAF].leaf + devices = [] + + devices.append(LeafSensor(controller, hass.data[LeafCore.DATA_LEAF])) + + add_devices(devices, True) + +class LeafSensor(Entity): + def __init__(self, controller, data): + self.controller = controller + self.data = data + + @property + def name(self): + return "Leaf Charge %" + + @property + def state(self): + return self.data[LeafCore.DATA_BATTERY] + + @property + def unit_of_measurement(self): + return '%' + + def update(self): + self.data.update() \ No newline at end of file diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py new file mode 100644 index 0000000000000..bc913400fa323 --- /dev/null +++ b/homeassistant/components/switch/nissan_leaf.py @@ -0,0 +1,71 @@ +""" +Charge and Climate Switch for Nissan Leaf +""" + +import logging +from datetime import timedelta + +from homeassistant.components.sensor import ENTITY_ID_FORMAT +import custom_components.leaf as LeafCore +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + +def setup_platform(hass, config, add_devices, discovery_info=None): + controller = hass.data[LeafCore.DATA_LEAF].leaf + devices = [] + + devices.append(LeafChargeSwitch(controller, hass.data[LeafCore.DATA_LEAF])) + devices.append(LeafClimateSwitch(controller, hass.data[LeafCore.DATA_LEAF])) + + add_devices(devices, True) + +class LeafClimateSwitch(Entity): + def __init__(self, controller, data): + self.controller = controller + self.data = data + + @property + def name(self): + return "Leaf Climate Control" + + @property + def is_on(self): + return self.data[LeafCore.DATA_CLIMATE] + + def turn_on(self): + if self.controller.set_climate(True): + self.data[LeafCore.DATA_CLIMATE] = True + + def turn_off(self): + if self.controller.set_climate(False): + self.data[LeafCore.DATA_CLIMATE] = False + + def update(self): + self.data.update() + + +class LeafChargeSwitch(Entity): + def __init__(self, controller, data): + self.controller = controller + self.data = data + + @property + def name(self): + return "Leaf Charging Status" + + @property + def is_on(self): + return self.data[LeafCore.DATA_CHARGING] + + def turn_on(self): + if self.controller.start_charging() + self.data[LeafCore.DATA_CHARGING] = True + + def turn_off(self): + _LOGGER.debug("Cannot turn off Leaf charging - Nissan does not support that remotely.") + + def update(self): + self.data.update() From 7f80bd0227ae938f8c2838a008c9cac27fcffaaa Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Thu, 11 Jan 2018 16:40:23 +0000 Subject: [PATCH 02/78] Change interval so nobody drains their battery when I put this online --- homeassistant/components/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index e042b38e2c6c4..94abd01358116 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -22,7 +22,7 @@ DATA_CLIMATE = 'climate' CONF_INTERVAL = 'update_interval' -DEFAULT_INTERVAL = 1 +DEFAULT_INTERVAL = 30 CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ From 18f91f8b68617c24e281b65a013c41c51f0bb3ce Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Thu, 11 Jan 2018 17:13:51 +0000 Subject: [PATCH 03/78] Added the warning notice. --- homeassistant/components/nissan_leaf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 94abd01358116..4c400f113c7f8 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -53,6 +53,8 @@ def setup(hass, config): _LOGGER.error("Unable to fetch car details... do you actually have a Leaf connected to your account?") return False + _LOGGER.Info("WARNING: This component may poll your Leaf too often, and drain the 12V. If you drain your car's 12V it won't start as the drive train battery won't connect, so you have been warned.") + hass.data[DATA_LEAF] = LeafDataStore(leaf) for component in LEAF_COMPONENTS: From 7080aac1f9d1b8f10974edca4bc794a3232a807f Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Fri, 19 Jan 2018 16:24:42 +0000 Subject: [PATCH 04/78] Async setup --- homeassistant/components/nissan_leaf.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 4c400f113c7f8..2862143306523 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -7,6 +7,8 @@ from homeassistant.util import Throttle from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers.discovery import load_platform +import asyncio + REQUIREMENTS = ['https://github.com/BenWoodford/pycarwings2/archive/master.zip' '#pycarwings'] @@ -36,15 +38,18 @@ 'sensor', 'switch'#, 'device_tracker', ] -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] import pycarwings2 + _LOGGER.debug("Logging into You+Nissan...") + try: s = pycarwings2.Session(username, password, "NE") - _LOGGER.info("Logging into CarWings/NissanConnect Account") + _LOGGER.Info("Fetching Leaf Data") leaf = s.get_leaf() except(RuntimeError, urllib.error.HTTPError): _LOGGER.error("Unable to connect to Nissan Connect with username and password") @@ -52,7 +57,10 @@ def setup(hass, config): except(KeyError): _LOGGER.error("Unable to fetch car details... do you actually have a Leaf connected to your account?") return False + except: + _LOGGER.error("An unknown error occurred while connecting to Nissan's servers: " + sys.exc_info()[0]) + _LOGGER.Info("Successfully logged in and fetched Leaf info") _LOGGER.Info("WARNING: This component may poll your Leaf too often, and drain the 12V. If you drain your car's 12V it won't start as the drive train battery won't connect, so you have been warned.") hass.data[DATA_LEAF] = LeafDataStore(leaf) From 3aa168be3c3208a5a9a8b997baecd0954c1e45f6 Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Mon, 22 Jan 2018 18:01:41 +0000 Subject: [PATCH 05/78] Still broken, but we're getting there. --- homeassistant/components/nissan_leaf.py | 156 ++++++++++++++---- .../components/sensor/nissan_leaf.py | 32 ++-- .../components/switch/nissan_leaf.py | 65 ++++---- 3 files changed, 176 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 2862143306523..5c6fe8c7c4b55 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -1,13 +1,20 @@ import voluptuous as vol import logging from datetime import timedelta +import time import urllib import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity import asyncio +import sys +from homeassistant.helpers.event import track_time_interval +from homeassistant.util.async import fire_coroutine_threadsafe REQUIREMENTS = ['https://github.com/BenWoodford/pycarwings2/archive/master.zip' @@ -23,23 +30,27 @@ DATA_CHARGING = 'charging' DATA_CLIMATE = 'climate' +CONF_NCONNECT = 'nissan_connect' CONF_INTERVAL = 'update_interval' -DEFAULT_INTERVAL = 30 +DEFAULT_INTERVAL = timedelta(minutes=30) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NCONNECT, default=True): cv.boolean, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int }) }, extra=vol.ALLOW_EXTRA) LEAF_COMPONENTS = [ - 'sensor', 'switch'#, 'device_tracker', + 'sensor', 'switch', 'device_tracker', ] -@asyncio.coroutine -def async_setup(hass, config): +SIGNAL_UPDATE_LEAF = 'nissan_leaf_update' + + +def setup(hass, config): username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] @@ -49,89 +60,164 @@ def async_setup(hass, config): try: s = pycarwings2.Session(username, password, "NE") - _LOGGER.Info("Fetching Leaf Data") + _LOGGER.debug("Fetching Leaf Data") leaf = s.get_leaf() except(RuntimeError, urllib.error.HTTPError): - _LOGGER.error("Unable to connect to Nissan Connect with username and password") + _LOGGER.error( + "Unable to connect to Nissan Connect with username and password") return False except(KeyError): - _LOGGER.error("Unable to fetch car details... do you actually have a Leaf connected to your account?") + _LOGGER.error( + "Unable to fetch car details... do you actually have a Leaf connected to your account?") return False except: - _LOGGER.error("An unknown error occurred while connecting to Nissan's servers: " + sys.exc_info()[0]) + _LOGGER.error( + "An unknown error occurred while connecting to Nissan's servers: ", sys.exc_info()[0]) - _LOGGER.Info("Successfully logged in and fetched Leaf info") - _LOGGER.Info("WARNING: This component may poll your Leaf too often, and drain the 12V. If you drain your car's 12V it won't start as the drive train battery won't connect, so you have been warned.") + _LOGGER.info("Successfully logged in and fetched Leaf info") + _LOGGER.info("WARNING: This component may poll your Leaf too often, and drain the 12V. If you drain your car's 12V it won't start as the drive train battery won't connect, so you have been warned.") - hass.data[DATA_LEAF] = LeafDataStore(leaf) + hass.data[DATA_LEAF] = {} + hass.data[DATA_LEAF][leaf.vin] = LeafDataStore( + leaf, hass, config[DOMAIN][CONF_NCONNECT]) for component in LEAF_COMPONENTS: - load_platform(hass, component, DOMAIN, {}, config) + if (component != 'device_tracker') or (config[DOMAIN][CONF_NCONNECT] == True): + load_platform(hass, component, DOMAIN, {}, config) + + def refresh_leaf_if_necessary(event_time): + _LOGGER.debug("Interval fired, refreshing data...") + fire_coroutine_threadsafe( + hass.data[DATA_LEAF][leaf.vin].async_update_leaf(), hass.loop) + + track_time_interval( + hass, refresh_leaf_if_necessary, DEFAULT_INTERVAL) + + refresh_leaf_if_necessary(0) return True + class LeafDataStore: - def __init__(self, leaf): + def __init__(self, leaf, hass, use_nissan_connect): self.leaf = leaf + self.nissan_connect = use_nissan_connect + self.hass = hass self.data = {} + self.data[DATA_CLIMATE] = False + self.data[DATA_BATTERY] = 0 + self.data[DATA_CHARGING] = False + self.data[DATA_LOCATION] = False + + @asyncio.coroutine + def async_update_leaf(self): + _LOGGER.debug("Updating Nissan Leaf Data") + + batteryResponse = yield from self.get_battery() + _LOGGER.debug("Got battery data for Leaf") - @Throttle(timedelta(minutes=DEFAULT_INTERVAL)) - def update(self): - batteryResponse = self.get_battery() if batteryResponse.answer == 200: - self.data[DATA_BATTERY] = 0 + self.data[DATA_BATTERY] = round(batteryResponse.battery_percent, 0) + #self.data[DATA_BATTERY] = 1 self.data[DATA_CHARGING] = batteryResponse.is_charging + _LOGGER.debug("Battery Response: ") + _LOGGER.debug(batteryResponse.__dict__) + climateResponse = self.get_climate() - self.data[DATA_CLIMATE] = climateResponse.is_hvac_running + _LOGGER.debug("Got climate data for Leaf") + self.data[DATA_CLIMATE] = climateResponse + + if self.nissan_connect: + try: + locationResponse = self.get_location() + + if locationResponse is None: + _LOGGER.debug("Empty Location Response Received") + self.data[DATA_LOCATION] = None + else: + LOGGER.debug("Got location data for Leaf") + self.data[DATA_LOCATION] = locationResponse + + _LOGGER.debug("Location Response: ") + _LOGGER.debug(locationResponse.__dict__) + except Exception as e: + _LOGGER.error("Error fetching location info") - locationResponse = self.get_location() - self.data[DATA_LOCATION] = locationResponse + _LOGGER.debug("Notifying Components") + dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) + @asyncio.coroutine def get_battery(self): request = self.leaf.request_update() battery_status = self.leaf.get_status_from_update(request) while battery_status is None: - time.sleep(5) + _LOGGER.debug("Battery data not in yet.") + yield from asyncio.sleep(5) battery_status = self.leaf.get_status_from_update(request) - _LOGGER.info(battery_status) - return battery_status def get_climate(self): - return None + request = self.leaf.get_latest_hvac_status() + if request is None: + return False + else: + return request.is_hvac_running def set_climate(self, toggle): if toggle: request = self.leaf.start_climate_control() - climate_result = self.leaf.get_start_climate_control_result(request) - + climate_result = self.leaf.get_start_climate_control_result( + request) + while climate_result is None: + _LOGGER.debug("Climate data not in yet.") time.sleep(5) - climate_result = self.leaf.get_start_climate_control_result(request) + climate_result = self.leaf.get_start_climate_control_result( + request) return climate_result.is_hvac_running else: request = self.leaf.stop_climate_control() climate_result = self.leaf.get_stop_climate_control_result(request) - + while climate_result is None: + _LOGGER.debug("Climate data not in yet.") time.sleep(5) - climate_result = self.leaf.get_stop_climate_control_result(request) + climate_result = self.leaf.get_stop_climate_control_result( + request) - return not climate_result.is_hvac_running + return climate_result.is_hvac_running def get_location(self): - request = self.left.request_location() + request = self.leaf.request_location() location_status = self.leaf.get_status_from_location(request) while location_status is None: - time.sleep(5) + _LOGGER.debug("Location data not in yet.") + yield from asyncio.sleep(5) location_status = self.leaf.get_status_from_location(request) - + return location_status - + def start_charging(self): - return self.leaf.start_charging() \ No newline at end of file + return self.leaf.start_charging() + + +class LeafEntity(Entity): + def __init__(self, controller, data): + self.controller = controller + self.data = data + + def added_to_hass(self): + """Register callbacks.""" + self.log_registration() + async_dispatcher_connect( + self.data.hass, SIGNAL_UPDATE_LEAF, self._update_callback) + + def _update_callback(self): + """Callback update method.""" + _LOGGER.debug("Got dispatcher update from Leaf platform") + self.schedule_update_ha_state(True) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 70bbd0e839866..c5ab1b4a55415 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -6,37 +6,45 @@ from datetime import timedelta from homeassistant.components.sensor import ENTITY_ID_FORMAT -import custom_components.leaf as LeafCore +import homeassistant.components.nissan_leaf as LeafCore +from homeassistant.components.nissan_leaf import LeafEntity from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +import asyncio + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['nissan_leaf'] + def setup_platform(hass, config, add_devices, discovery_info=None): - controller = hass.data[LeafCore.DATA_LEAF].leaf devices = [] - devices.append(LeafSensor(controller, hass.data[LeafCore.DATA_LEAF])) + for key, value in hass.data[LeafCore.DATA_LEAF].items(): + devices.append(LeafBatterySensor(value.leaf, value)) + + _LOGGER.debug("Adding sensors") add_devices(devices, True) -class LeafSensor(Entity): - def __init__(self, controller, data): - self.controller = controller - self.data = data + return True + +class LeafBatterySensor(LeafCore.LeafEntity): @property def name(self): return "Leaf Charge %" - + + def log_registration(self): + _LOGGER.debug( + "Registered LeafBatterySensor component with HASS for VIN " + self.controller.vin) + @property def state(self): - return self.data[LeafCore.DATA_BATTERY] + return self.data.data[LeafCore.DATA_BATTERY] @property def unit_of_measurement(self): return '%' - - def update(self): - self.data.update() \ No newline at end of file diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index bc913400fa323..eaed1083f6e9d 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -6,66 +6,71 @@ from datetime import timedelta from homeassistant.components.sensor import ENTITY_ID_FORMAT -import custom_components.leaf as LeafCore +import homeassistant.components.nissan_leaf as LeafCore from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +import asyncio + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['nissan_leaf'] + def setup_platform(hass, config, add_devices, discovery_info=None): - controller = hass.data[LeafCore.DATA_LEAF].leaf devices = [] - devices.append(LeafChargeSwitch(controller, hass.data[LeafCore.DATA_LEAF])) - devices.append(LeafClimateSwitch(controller, hass.data[LeafCore.DATA_LEAF])) + for key, value in hass.data[LeafCore.DATA_LEAF].items(): + devices.append(LeafChargeSwitch(value.leaf, value)) + devices.append(LeafClimateSwitch( + value.leaf, value)) add_devices(devices, True) -class LeafClimateSwitch(Entity): - def __init__(self, controller, data): - self.controller = controller - self.data = data +class LeafClimateSwitch(LeafCore.LeafEntity): @property def name(self): return "Leaf Climate Control" - + + def log_registration(self): + _LOGGER.debug( + "Registered LeafClimateSwitch component with HASS for VIN " + self.controller.vin) + @property def is_on(self): - return self.data[LeafCore.DATA_CLIMATE] + return self.data.data[LeafCore.DATA_CLIMATE] def turn_on(self): - if self.controller.set_climate(True): - self.data[LeafCore.DATA_CLIMATE] = True + # if self.controller.set_climate(True): + #self.data[LeafCore.DATA_CLIMATE] = True + _LOGGER.info("Climate Control is not implemented yet.") def turn_off(self): - if self.controller.set_climate(False): - self.data[LeafCore.DATA_CLIMATE] = False - - def update(self): - self.data.update() + _LOGGER.info("Climate Control is not implemented yet.") + # if self.controller.set_climate(False): + #self.data[LeafCore.DATA_CLIMATE] = False -class LeafChargeSwitch(Entity): - def __init__(self, controller, data): - self.controller = controller - self.data = data - +class LeafChargeSwitch(LeafCore.LeafEntity): @property def name(self): return "Leaf Charging Status" - + + def log_registration(self): + _LOGGER.debug( + "Registered LeafChargeSwitch component with HASS for VIN " + self.controller.vin) + @property def is_on(self): - return self.data[LeafCore.DATA_CHARGING] + return self.data.data[LeafCore.DATA_CHARGING] def turn_on(self): - if self.controller.start_charging() - self.data[LeafCore.DATA_CHARGING] = True + _LOGGER.info("Charging is not implemented yet.") + # if self.controller.start_charging(): + #self.data[LeafCore.DATA_CHARGING] = True def turn_off(self): - _LOGGER.debug("Cannot turn off Leaf charging - Nissan does not support that remotely.") - - def update(self): - self.data.update() + _LOGGER.debug( + "Cannot turn off Leaf charging - Nissan does not support that remotely.") From aa500308f7c73c71217634d29d711140d80b191f Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Tue, 23 Jan 2018 11:50:06 +0000 Subject: [PATCH 06/78] Back to synchronous, moved refresh stuff into DataStore --- homeassistant/components/nissan_leaf.py | 26 +++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 5c6fe8c7c4b55..7d374965c2d97 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -85,15 +85,7 @@ def setup(hass, config): if (component != 'device_tracker') or (config[DOMAIN][CONF_NCONNECT] == True): load_platform(hass, component, DOMAIN, {}, config) - def refresh_leaf_if_necessary(event_time): - _LOGGER.debug("Interval fired, refreshing data...") - fire_coroutine_threadsafe( - hass.data[DATA_LEAF][leaf.vin].async_update_leaf(), hass.loop) - - track_time_interval( - hass, refresh_leaf_if_necessary, DEFAULT_INTERVAL) - - refresh_leaf_if_necessary(0) + hass.data[DATA_LEAF][leaf.vin].refresh_leaf_if_necessary(0) return True @@ -109,9 +101,14 @@ def __init__(self, leaf, hass, use_nissan_connect): self.data[DATA_BATTERY] = 0 self.data[DATA_CHARGING] = False self.data[DATA_LOCATION] = False + track_time_interval( + hass, self.refresh_leaf_if_necessary, DEFAULT_INTERVAL) + + def refresh_leaf_if_necessary(self, event_time): + _LOGGER.debug("Interval fired, refreshing data...") + self.update_leaf() - @asyncio.coroutine - def async_update_leaf(self): + def update_leaf(self): _LOGGER.debug("Updating Nissan Leaf Data") batteryResponse = yield from self.get_battery() @@ -119,7 +116,7 @@ def async_update_leaf(self): if batteryResponse.answer == 200: self.data[DATA_BATTERY] = round(batteryResponse.battery_percent, 0) - #self.data[DATA_BATTERY] = 1 + # self.data[DATA_BATTERY] = 1 self.data[DATA_CHARGING] = batteryResponse.is_charging _LOGGER.debug("Battery Response: ") @@ -148,13 +145,12 @@ def async_update_leaf(self): _LOGGER.debug("Notifying Components") dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) - @asyncio.coroutine def get_battery(self): request = self.leaf.request_update() battery_status = self.leaf.get_status_from_update(request) while battery_status is None: _LOGGER.debug("Battery data not in yet.") - yield from asyncio.sleep(5) + time.sleep(5) battery_status = self.leaf.get_status_from_update(request) return battery_status @@ -197,7 +193,7 @@ def get_location(self): while location_status is None: _LOGGER.debug("Location data not in yet.") - yield from asyncio.sleep(5) + time.sleep(5) location_status = self.leaf.get_status_from_location(request) return location_status From b01060fd9ac8228e9185ce5ddb693e06f0d5c2b6 Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Tue, 23 Jan 2018 14:39:59 +0000 Subject: [PATCH 07/78] Functional sensors! --- homeassistant/components/nissan_leaf.py | 60 +++++++++------- .../components/sensor/nissan_leaf.py | 68 ++++++++++++++++--- .../components/switch/nissan_leaf.py | 35 +++++----- 3 files changed, 112 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 7d374965c2d97..5eb976bef7ec1 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -28,16 +28,22 @@ DATA_BATTERY = 'battery' DATA_LOCATION = 'location' DATA_CHARGING = 'charging' +DATA_PLUGGED_IN = 'plugged_in' DATA_CLIMATE = 'climate' +DATA_RANGE_AC = 'range_ac_on' +DATA_RANGE_AC_OFF = 'range_ac_off' CONF_NCONNECT = 'nissan_connect' CONF_INTERVAL = 'update_interval' -DEFAULT_INTERVAL = timedelta(minutes=30) +CONF_REGION = 'region' +CONF_VALID_REGIONS = ['NNA', 'NE', 'NCI', 'NMA', 'NML'] +DEFAULT_INTERVAL = 30 CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS), vol.Optional(CONF_NCONNECT, default=True): cv.boolean, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int }) @@ -59,7 +65,8 @@ def setup(hass, config): _LOGGER.debug("Logging into You+Nissan...") try: - s = pycarwings2.Session(username, password, "NE") + s = pycarwings2.Session( + username, password, config[DOMAIN][CONF_REGION]) _LOGGER.debug("Fetching Leaf Data") leaf = s.get_leaf() except(RuntimeError, urllib.error.HTTPError): @@ -79,7 +86,7 @@ def setup(hass, config): hass.data[DATA_LEAF] = {} hass.data[DATA_LEAF][leaf.vin] = LeafDataStore( - leaf, hass, config[DOMAIN][CONF_NCONNECT]) + leaf, hass, config) for component in LEAF_COMPONENTS: if (component != 'device_tracker') or (config[DOMAIN][CONF_NCONNECT] == True): @@ -92,39 +99,45 @@ def setup(hass, config): class LeafDataStore: - def __init__(self, leaf, hass, use_nissan_connect): + def __init__(self, leaf, hass, config): self.leaf = leaf - self.nissan_connect = use_nissan_connect + self.config = config + self.nissan_connect = config[DOMAIN][CONF_NCONNECT] self.hass = hass self.data = {} self.data[DATA_CLIMATE] = False - self.data[DATA_BATTERY] = 0 + self.data[DATA_BATTERY] = 1 self.data[DATA_CHARGING] = False self.data[DATA_LOCATION] = False track_time_interval( - hass, self.refresh_leaf_if_necessary, DEFAULT_INTERVAL) + hass, self.refresh_leaf_if_necessary, timedelta(minutes=config[DOMAIN][CONF_INTERVAL])) def refresh_leaf_if_necessary(self, event_time): _LOGGER.debug("Interval fired, refreshing data...") - self.update_leaf() + self.refresh_data() - def update_leaf(self): + def refresh_data(self): _LOGGER.debug("Updating Nissan Leaf Data") - batteryResponse = yield from self.get_battery() + batteryResponse = self.get_battery() _LOGGER.debug("Got battery data for Leaf") - if batteryResponse.answer == 200: - self.data[DATA_BATTERY] = round(batteryResponse.battery_percent, 0) - # self.data[DATA_BATTERY] = 1 + if batteryResponse.answer['status'] == 200: + self.data[DATA_BATTERY] = batteryResponse.battery_percent self.data[DATA_CHARGING] = batteryResponse.is_charging + self.data[DATA_PLUGGED_IN] = batteryResponse.is_connected + self.data[DATA_RANGE_AC] = batteryResponse.cruising_range_ac_on_km + self.data[DATA_RANGE_AC_OFF] = batteryResponse.cruising_range_ac_off_km _LOGGER.debug("Battery Response: ") _LOGGER.debug(batteryResponse.__dict__) climateResponse = self.get_climate() - _LOGGER.debug("Got climate data for Leaf") - self.data[DATA_CLIMATE] = climateResponse + + if climateResponse is not None: + _LOGGER.debug("Got climate data for Leaf") + _LOGGER.debug(climateResponse.__dict__) + self.data[DATA_CLIMATE] = climateResponse.is_hvac_running if self.nissan_connect: try: @@ -157,10 +170,7 @@ def get_battery(self): def get_climate(self): request = self.leaf.get_latest_hvac_status() - if request is None: - return False - else: - return request.is_hvac_running + return request def set_climate(self, toggle): if toggle: @@ -203,17 +213,17 @@ def start_charging(self): class LeafEntity(Entity): - def __init__(self, controller, data): - self.controller = controller - self.data = data + def __init__(self, car): + self.car = car - def added_to_hass(self): + @asyncio.coroutine + def async_added_to_hass(self): """Register callbacks.""" self.log_registration() async_dispatcher_connect( - self.data.hass, SIGNAL_UPDATE_LEAF, self._update_callback) + self.car.hass, SIGNAL_UPDATE_LEAF, self._update_callback) def _update_callback(self): """Callback update method.""" - _LOGGER.debug("Got dispatcher update from Leaf platform") + #_LOGGER.debug("Got dispatcher update from Leaf platform") self.schedule_update_ha_state(True) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index c5ab1b4a55415..574707e5758b4 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -6,13 +6,13 @@ from datetime import timedelta from homeassistant.components.sensor import ENTITY_ID_FORMAT -import homeassistant.components.nissan_leaf as LeafCore -from homeassistant.components.nissan_leaf import LeafEntity +from .. import nissan_leaf as LeafCore +from ..nissan_leaf import LeafEntity from homeassistant.helpers.entity import Entity from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) import asyncio - +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM _LOGGER = logging.getLogger(__name__) @@ -22,11 +22,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] - for key, value in hass.data[LeafCore.DATA_LEAF].items(): - devices.append(LeafBatterySensor(value.leaf, value)) - _LOGGER.debug("Adding sensors") + for key, value in hass.data[LeafCore.DATA_LEAF].items(): + devices.append(LeafBatterySensor(value)) + devices.append(LeafPluggedInSensor(value)) + devices.append(LeafRangeSensor(value, True)) + devices.append(LeafRangeSensor(value, False)) + add_devices(devices, True) return True @@ -39,12 +42,61 @@ def name(self): def log_registration(self): _LOGGER.debug( - "Registered LeafBatterySensor component with HASS for VIN " + self.controller.vin) + "Registered LeafBatterySensor component with HASS for VIN " + self.car.leaf.vin) @property def state(self): - return self.data.data[LeafCore.DATA_BATTERY] + return round(self.car.data[LeafCore.DATA_BATTERY], 0) @property def unit_of_measurement(self): return '%' + + +class LeafPluggedInSensor(LeafCore.LeafEntity): + @property + def name(self): + return "Leaf Plugged In" + + def log_registration(self): + _LOGGER.debug( + "Registered LeafPluggedInSensor component with HASS for VIN " + self.car.leaf.vin) + + @property + def state(self): + return self.car.data[LeafCore.DATA_PLUGGED_IN] + + +class LeafRangeSensor(LeafCore.LeafEntity): + def __init__(self, car, ac_on): + self.ac_on = ac_on + super().__init__(car) + + @property + def name(self): + return "Range (" + ("AC On" if self.ac_on == True else "AC Off") + ")" + + def log_registration(self): + _LOGGER.debug( + "Registered LeafRangeSensor component with HASS for VIN " + self.car.leaf.vin) + + @property + def state(self): + ret = 0 + + if self.ac_on == True: + ret = self.car.data[LeafCore.DATA_RANGE_AC] + else: + ret = self.car.data[LeafCore.DATA_RANGE_AC_OFF] + + if self.car.hass.config.units.is_metric == False: + ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) + + return round(ret, 0) + + @property + def unit_of_measurement(self): + if self.car.hass.config.units.is_metric: + return "km" + else: + return "mi" diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index eaed1083f6e9d..ef7ad47ffb330 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -6,8 +6,8 @@ from datetime import timedelta from homeassistant.components.sensor import ENTITY_ID_FORMAT -import homeassistant.components.nissan_leaf as LeafCore -from homeassistant.helpers.entity import Entity +from .. import nissan_leaf as LeafCore +from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) import asyncio @@ -22,55 +22,54 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for key, value in hass.data[LeafCore.DATA_LEAF].items(): - devices.append(LeafChargeSwitch(value.leaf, value)) - devices.append(LeafClimateSwitch( - value.leaf, value)) + devices.append(LeafChargeSwitch(value)) + devices.append(LeafClimateSwitch(value)) add_devices(devices, True) -class LeafClimateSwitch(LeafCore.LeafEntity): +class LeafClimateSwitch(LeafCore.LeafEntity, ToggleEntity): @property def name(self): return "Leaf Climate Control" def log_registration(self): _LOGGER.debug( - "Registered LeafClimateSwitch component with HASS for VIN " + self.controller.vin) + "Registered LeafClimateSwitch component with HASS for VIN " + self.car.leaf.vin) @property def is_on(self): - return self.data.data[LeafCore.DATA_CLIMATE] + return self.car.data[LeafCore.DATA_CLIMATE] == True - def turn_on(self): + def turn_on(self, **kwargs): # if self.controller.set_climate(True): - #self.data[LeafCore.DATA_CLIMATE] = True + # self.data[LeafCore.DATA_CLIMATE] = True _LOGGER.info("Climate Control is not implemented yet.") - def turn_off(self): + def turn_off(self, **kwargs): _LOGGER.info("Climate Control is not implemented yet.") # if self.controller.set_climate(False): - #self.data[LeafCore.DATA_CLIMATE] = False + # self.data[LeafCore.DATA_CLIMATE] = False -class LeafChargeSwitch(LeafCore.LeafEntity): +class LeafChargeSwitch(LeafCore.LeafEntity, ToggleEntity): @property def name(self): return "Leaf Charging Status" def log_registration(self): _LOGGER.debug( - "Registered LeafChargeSwitch component with HASS for VIN " + self.controller.vin) + "Registered LeafChargeSwitch component with HASS for VIN " + self.car.leaf.vin) @property def is_on(self): - return self.data.data[LeafCore.DATA_CHARGING] + return self.car.data[LeafCore.DATA_CHARGING] == True - def turn_on(self): + def turn_on(self, entity_id): _LOGGER.info("Charging is not implemented yet.") # if self.controller.start_charging(): - #self.data[LeafCore.DATA_CHARGING] = True + # self.data[LeafCore.DATA_CHARGING] = True - def turn_off(self): + def turn_off(self, entity_id): _LOGGER.debug( "Cannot turn off Leaf charging - Nissan does not support that remotely.") From d36084697ff9bca5033ebdec71c82d63c08d4508 Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Tue, 23 Jan 2018 15:55:51 +0000 Subject: [PATCH 08/78] Added working switches, tweaked intervals a bit --- homeassistant/components/nissan_leaf.py | 11 ++++++++ .../components/sensor/nissan_leaf.py | 7 ++++-- .../components/switch/nissan_leaf.py | 25 +++++++++++-------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 5eb976bef7ec1..64eb644e7dd0e 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -156,6 +156,9 @@ def refresh_data(self): _LOGGER.error("Error fetching location info") _LOGGER.debug("Notifying Components") + self.signal_components() + + def signal_components(self): dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) def get_battery(self): @@ -184,6 +187,10 @@ def set_climate(self, toggle): climate_result = self.leaf.get_start_climate_control_result( request) + _LOGGER.debug(climate_result.__dict__) + + self.signal_components() + return climate_result.is_hvac_running else: request = self.leaf.stop_climate_control() @@ -195,6 +202,10 @@ def set_climate(self, toggle): climate_result = self.leaf.get_stop_climate_control_result( request) + _LOGGER.debug(climate_result.__dict__) + + self.signal_components() + return climate_result.is_hvac_running def get_location(self): diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 574707e5758b4..6f2202a4c4cf5 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LeafBatterySensor(LeafCore.LeafEntity): @property def name(self): - return "Leaf Charge %" + return "Leaf Charge" def log_registration(self): _LOGGER.debug( @@ -74,7 +74,10 @@ def __init__(self, car, ac_on): @property def name(self): - return "Range (" + ("AC On" if self.ac_on == True else "AC Off") + ")" + if self.ac_on == True: + return "Leaf Range (AC)" + else: + return "Leaf Range" def log_registration(self): _LOGGER.debug( diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index ef7ad47ffb330..3e9565a059c59 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -42,14 +42,16 @@ def is_on(self): return self.car.data[LeafCore.DATA_CLIMATE] == True def turn_on(self, **kwargs): - # if self.controller.set_climate(True): - # self.data[LeafCore.DATA_CLIMATE] = True - _LOGGER.info("Climate Control is not implemented yet.") + if self.car.set_climate(True): + self.car.data[LeafCore.DATA_CLIMATE] = True + + self._update_callback() def turn_off(self, **kwargs): - _LOGGER.info("Climate Control is not implemented yet.") - # if self.controller.set_climate(False): - # self.data[LeafCore.DATA_CLIMATE] = False + if self.car.set_climate(False): + self.car.data[LeafCore.DATA_CLIMATE] = False + + self._update_callback() class LeafChargeSwitch(LeafCore.LeafEntity, ToggleEntity): @@ -65,11 +67,12 @@ def log_registration(self): def is_on(self): return self.car.data[LeafCore.DATA_CHARGING] == True - def turn_on(self, entity_id): - _LOGGER.info("Charging is not implemented yet.") - # if self.controller.start_charging(): - # self.data[LeafCore.DATA_CHARGING] = True + def turn_on(self, **kwargs): + if self.car.start_charging(): + self.car.data[LeafCore.DATA_CHARGING] = True - def turn_off(self, entity_id): + self._update_callback() + + def turn_off(self, **kwargs): _LOGGER.debug( "Cannot turn off Leaf charging - Nissan does not support that remotely.") From 2d6016326555f4a4294e1d945cf6781b1fa69e1e Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Tue, 23 Jan 2018 15:58:38 +0000 Subject: [PATCH 09/78] Fixed turn off result --- homeassistant/components/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 64eb644e7dd0e..42e6a567f8387 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -206,7 +206,7 @@ def set_climate(self, toggle): self.signal_components() - return climate_result.is_hvac_running + return climate_result.is_hvac_running == False def get_location(self): request = self.leaf.request_location() From ad54f37b0029cf21f6880b0df9f78b4b1a8b5963 Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Wed, 31 Jan 2018 16:26:42 +0000 Subject: [PATCH 10/78] Moved plug status to binary_sensor, added smart intervals --- .../components/binary_sensor/nissan_leaf.py | 46 ++++++++++++++ homeassistant/components/nissan_leaf.py | 61 ++++++++++++++++--- .../components/sensor/nissan_leaf.py | 23 ++----- 3 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/binary_sensor/nissan_leaf.py diff --git a/homeassistant/components/binary_sensor/nissan_leaf.py b/homeassistant/components/binary_sensor/nissan_leaf.py new file mode 100644 index 0000000000000..7f097dbc2758a --- /dev/null +++ b/homeassistant/components/binary_sensor/nissan_leaf.py @@ -0,0 +1,46 @@ +""" +Plugged In Sensor for Nissan Leaf +""" + +import logging +from datetime import timedelta + +from homeassistant.components.sensor import ENTITY_ID_FORMAT +from .. import nissan_leaf as LeafCore +from ..nissan_leaf import LeafEntity +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +import asyncio +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + devices = [] + + _LOGGER.debug("Adding sensors") + + for key, value in hass.data[LeafCore.DATA_LEAF].items(): + devices.append(LeafPluggedInSensor(value)) + + add_devices(devices, True) + + return True + + +class LeafPluggedInSensor(LeafCore.LeafEntity): + @property + def name(self): + return "Leaf Plugged In" + + def log_registration(self): + _LOGGER.debug( + "Registered LeafPluggedInSensor component with HASS for VIN " + self.car.leaf.vin) + + @property + def state(self): + return self.car.data[LeafCore.DATA_PLUGGED_IN] diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 42e6a567f8387..93f8001f3e005 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -1,6 +1,6 @@ import voluptuous as vol import logging -from datetime import timedelta +from datetime import timedelta, datetime import time import urllib @@ -35,9 +35,16 @@ CONF_NCONNECT = 'nissan_connect' CONF_INTERVAL = 'update_interval' +CONF_CHARGING_INTERVAL = 'update_interval_charging' +CONF_CLIMATE_INTERVAL = 'update_interval_climate' CONF_REGION = 'region' CONF_VALID_REGIONS = ['NNA', 'NE', 'NCI', 'NMA', 'NML'] +CONF_FORCE_MILES = 'force_miles' DEFAULT_INTERVAL = 30 +DEFAULT_CHARGING_INTERVAL = 15 +DEFAULT_CLIMATE_INTERVAL = 5 + +CHECK_INTERVAL = 10 CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -45,12 +52,15 @@ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS), vol.Optional(CONF_NCONNECT, default=True): cv.boolean, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, + vol.Optional(CONF_CHARGING_INTERVAL, default=DEFAULT_CHARGING_INTERVAL): cv.positive_int, + vol.Optional(CONF_CLIMATE_INTERVAL, default=DEFAULT_CLIMATE_INTERVAL): cv.positive_int, + vol.Optional(CONF_FORCE_MILES, default=False): cv.boolean }) }, extra=vol.ALLOW_EXTRA) LEAF_COMPONENTS = [ - 'sensor', 'switch', 'device_tracker', + 'sensor', 'switch', 'binary_sensor' ] SIGNAL_UPDATE_LEAF = 'nissan_leaf_update' @@ -92,7 +102,7 @@ def setup(hass, config): if (component != 'device_tracker') or (config[DOMAIN][CONF_NCONNECT] == True): load_platform(hass, component, DOMAIN, {}, config) - hass.data[DATA_LEAF][leaf.vin].refresh_leaf_if_necessary(0) + # hass.data[DATA_LEAF][leaf.vin].refresh_leaf_if_necessary(0) return True @@ -106,19 +116,46 @@ def __init__(self, leaf, hass, config): self.hass = hass self.data = {} self.data[DATA_CLIMATE] = False - self.data[DATA_BATTERY] = 1 + self.data[DATA_BATTERY] = 0 self.data[DATA_CHARGING] = False self.data[DATA_LOCATION] = False + self.data[DATA_RANGE_AC] = 0 + self.data[DATA_RANGE_AC_OFF] = 0 + self.data[DATA_PLUGGED_IN] = False + self.lastCheck = None track_time_interval( - hass, self.refresh_leaf_if_necessary, timedelta(minutes=config[DOMAIN][CONF_INTERVAL])) + hass, self.refresh_leaf_if_necessary, timedelta(seconds=CHECK_INTERVAL)) def refresh_leaf_if_necessary(self, event_time): - _LOGGER.debug("Interval fired, refreshing data...") - self.refresh_data() + result = False + now = datetime.today() + + if self.lastCheck is None: + _LOGGER.debug("Firing Refresh on " + self.leaf.vin + + " as there has not been one yet.") + result = True + elif self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_INTERVAL]) < now: + _LOGGER.debug("Firing Refresh on " + self.leaf.vin + + " as the interval has passed.") + result = True + elif self.data[DATA_CHARGING] == True and self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_CHARGING_INTERVAL]) < now: + _LOGGER.debug("Firing Refresh on " + self.leaf.vin + + " as it's charging and the charging interval has passed.") + result = True + elif self.data[DATA_CLIMATE] == True and self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_CLIMATE_INTERVAL]) < now: + _LOGGER.debug("Firing Refresh on " + self.leaf.vin + + " as climate control is on and the interval has passed.") + result = True + + if result == True: + _LOGGER.debug("Interval fired, refreshing data...") + self.refresh_data() def refresh_data(self): _LOGGER.debug("Updating Nissan Leaf Data") + self.lastCheck = datetime.today() + batteryResponse = self.get_battery() _LOGGER.debug("Got battery data for Leaf") @@ -227,6 +264,14 @@ class LeafEntity(Entity): def __init__(self, car): self.car = car + @property + def device_state_attributes(self): + return { + 'homebridge_serial': self.car.leaf.vin, + 'homebridge_mfg': 'Nissan', + 'homebridge_model': 'Leaf' + } + @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 6f2202a4c4cf5..85b8bca717054 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -26,7 +26,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for key, value in hass.data[LeafCore.DATA_LEAF].items(): devices.append(LeafBatterySensor(value)) - devices.append(LeafPluggedInSensor(value)) devices.append(LeafRangeSensor(value, True)) devices.append(LeafRangeSensor(value, False)) @@ -53,20 +52,6 @@ def unit_of_measurement(self): return '%' -class LeafPluggedInSensor(LeafCore.LeafEntity): - @property - def name(self): - return "Leaf Plugged In" - - def log_registration(self): - _LOGGER.debug( - "Registered LeafPluggedInSensor component with HASS for VIN " + self.car.leaf.vin) - - @property - def state(self): - return self.car.data[LeafCore.DATA_PLUGGED_IN] - - class LeafRangeSensor(LeafCore.LeafEntity): def __init__(self, car, ac_on): self.ac_on = ac_on @@ -92,14 +77,14 @@ def state(self): else: ret = self.car.data[LeafCore.DATA_RANGE_AC_OFF] - if self.car.hass.config.units.is_metric == False: + if self.car.hass.config.units.is_metric == False or self.car.config[LeafCore.DOMAIN][LeafCore.CONF_FORCE_MILES] == True: ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) return round(ret, 0) @property def unit_of_measurement(self): - if self.car.hass.config.units.is_metric: - return "km" - else: + if self.car.hass.config.units.is_metric == False or self.car.config[LeafCore.DOMAIN][LeafCore.CONF_FORCE_MILES] == True: return "mi" + else: + return "km" From 3e20bd25cb31d0533a80711f39fa189c4989902a Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Thu, 1 Feb 2018 15:00:44 +0000 Subject: [PATCH 11/78] Documentation and car nickname stuff --- .../components/binary_sensor/nissan_leaf.py | 6 ++-- homeassistant/components/nissan_leaf.py | 31 ++++++++++++++++--- .../components/sensor/nissan_leaf.py | 10 +++--- .../components/switch/nissan_leaf.py | 8 +++-- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/binary_sensor/nissan_leaf.py b/homeassistant/components/binary_sensor/nissan_leaf.py index 7f097dbc2758a..55f590e6a5f75 100644 --- a/homeassistant/components/binary_sensor/nissan_leaf.py +++ b/homeassistant/components/binary_sensor/nissan_leaf.py @@ -1,5 +1,7 @@ """ -Plugged In Sensor for Nissan Leaf +Plugged In Status Support for the Nissan Leaf Carwings/Nissan Connect API. + +Documentation pending, please refer to the main platform API f """ import logging @@ -35,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LeafPluggedInSensor(LeafCore.LeafEntity): @property def name(self): - return "Leaf Plugged In" + return self.car.leaf.nickname + " Plug Status" def log_registration(self): _LOGGER.debug( diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 93f8001f3e005..44f4dc847c179 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -1,3 +1,22 @@ +""" +Support for the Nissan Leaf Carwings/Nissan Connect API. + +Please note this is the pre-2018 API, which is still functional in the US. +The old API should continue to work as the new one is not being released outside the US. + +Documentation pages have not been created yet, here is an example configuration block: + +nissan_leaf: + username: "username" + password: "password" + nissan_connect: false + region: 'NE' + update_interval: 30 + update_interval_charging: 15 + update_interval_climate: 5 + force_miles: true +""" + import voluptuous as vol import logging from datetime import timedelta, datetime @@ -5,7 +24,6 @@ import urllib import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import ( @@ -14,8 +32,9 @@ import asyncio import sys from homeassistant.helpers.event import track_time_interval -from homeassistant.util.async import fire_coroutine_threadsafe +# Currently waiting on the pycarwings2 author to pull this PR in and update pip, so using my fork for now. +# https://github.com/jdhorne/pycarwings2/pull/28 REQUIREMENTS = ['https://github.com/BenWoodford/pycarwings2/archive/master.zip' '#pycarwings'] @@ -148,7 +167,6 @@ def refresh_leaf_if_necessary(self, event_time): result = True if result == True: - _LOGGER.debug("Interval fired, refreshing data...") self.refresh_data() def refresh_data(self): @@ -176,6 +194,10 @@ def refresh_data(self): _LOGGER.debug(climateResponse.__dict__) self.data[DATA_CLIMATE] = climateResponse.is_hvac_running + +""" +# Removing this block for now, as I do not have a Leaf with Nissan Connect to test it with. + if self.nissan_connect: try: locationResponse = self.get_location() @@ -192,7 +214,8 @@ def refresh_data(self): except Exception as e: _LOGGER.error("Error fetching location info") - _LOGGER.debug("Notifying Components") +""" + self.signal_components() def signal_components(self): diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 85b8bca717054..67ccc1b1ea98f 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -1,5 +1,7 @@ """ -Battery Level Sensor for Nissan Leaf +Battery Charge and Range Support for the Nissan Leaf Carwings/Nissan Connect API. + +Documentation pending, please refer to the main platform component for configuration details """ import logging @@ -37,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LeafBatterySensor(LeafCore.LeafEntity): @property def name(self): - return "Leaf Charge" + return self.car.leaf.nickname + " Charge" def log_registration(self): _LOGGER.debug( @@ -60,9 +62,9 @@ def __init__(self, car, ac_on): @property def name(self): if self.ac_on == True: - return "Leaf Range (AC)" + return self.car.leaf.nickname + " Range (AC)" else: - return "Leaf Range" + return self.car.leaf.nickname + " Range" def log_registration(self): _LOGGER.debug( diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index 3e9565a059c59..fd1b97ab109de 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -1,5 +1,7 @@ """ -Charge and Climate Switch for Nissan Leaf +Charge and Climate Control Support for the Nissan Leaf Carwings/Nissan Connect API. + +Documentation pending, please refer to the main platform component for configuration details """ import logging @@ -31,7 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LeafClimateSwitch(LeafCore.LeafEntity, ToggleEntity): @property def name(self): - return "Leaf Climate Control" + return self.car.leaf.nickname + " Climate Control" def log_registration(self): _LOGGER.debug( @@ -57,7 +59,7 @@ def turn_off(self, **kwargs): class LeafChargeSwitch(LeafCore.LeafEntity, ToggleEntity): @property def name(self): - return "Leaf Charging Status" + return self.car.leaf.nickname + " Charging Status" def log_registration(self): _LOGGER.debug( From 0a64caeff963853ace4d1f11bbc1985e9d4c4e53 Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Thu, 1 Feb 2018 15:22:44 +0000 Subject: [PATCH 12/78] Syntax fixes and coveragerc additions --- .coveragerc | 3 +++ .../components/binary_sensor/nissan_leaf.py | 7 +------ homeassistant/components/nissan_leaf.py | 19 +++++++------------ .../components/sensor/nissan_leaf.py | 12 ++++-------- .../components/switch/nissan_leaf.py | 10 ++-------- 5 files changed, 17 insertions(+), 34 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3529e7413caa3..8733beb145a8e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -166,6 +166,9 @@ omit = homeassistant/components/netatmo.py homeassistant/components/*/netatmo.py + homeassistant/components/nissan_leaf.py + homeassistant/components/*/nissan_leaf.py + homeassistant/components/octoprint.py homeassistant/components/*/octoprint.py diff --git a/homeassistant/components/binary_sensor/nissan_leaf.py b/homeassistant/components/binary_sensor/nissan_leaf.py index 55f590e6a5f75..ff89182062259 100644 --- a/homeassistant/components/binary_sensor/nissan_leaf.py +++ b/homeassistant/components/binary_sensor/nissan_leaf.py @@ -1,19 +1,14 @@ """ Plugged In Status Support for the Nissan Leaf Carwings/Nissan Connect API. -Documentation pending, please refer to the main platform API f +Documentation pending, please refer to the main platform component for configuration details """ import logging -from datetime import timedelta -from homeassistant.components.sensor import ENTITY_ID_FORMAT from .. import nissan_leaf as LeafCore from ..nissan_leaf import LeafEntity from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) -import asyncio from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 44f4dc847c179..378f75b27784a 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -118,7 +118,7 @@ def setup(hass, config): leaf, hass, config) for component in LEAF_COMPONENTS: - if (component != 'device_tracker') or (config[DOMAIN][CONF_NCONNECT] == True): + if (component != 'device_tracker') or (config[DOMAIN][CONF_NCONNECT] is True): load_platform(hass, component, DOMAIN, {}, config) # hass.data[DATA_LEAF][leaf.vin].refresh_leaf_if_necessary(0) @@ -131,7 +131,8 @@ class LeafDataStore: def __init__(self, leaf, hass, config): self.leaf = leaf self.config = config - self.nissan_connect = config[DOMAIN][CONF_NCONNECT] + #self.nissan_connect = config[DOMAIN][CONF_NCONNECT] + self.nissan_connect = False # Disabled until tested and implemented self.hass = hass self.data = {} self.data[DATA_CLIMATE] = False @@ -157,16 +158,16 @@ def refresh_leaf_if_necessary(self, event_time): _LOGGER.debug("Firing Refresh on " + self.leaf.vin + " as the interval has passed.") result = True - elif self.data[DATA_CHARGING] == True and self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_CHARGING_INTERVAL]) < now: + elif self.data[DATA_CHARGING] is True and self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_CHARGING_INTERVAL]) < now: _LOGGER.debug("Firing Refresh on " + self.leaf.vin + " as it's charging and the charging interval has passed.") result = True - elif self.data[DATA_CLIMATE] == True and self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_CLIMATE_INTERVAL]) < now: + elif self.data[DATA_CLIMATE] is True and self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_CLIMATE_INTERVAL]) < now: _LOGGER.debug("Firing Refresh on " + self.leaf.vin + " as climate control is on and the interval has passed.") result = True - if result == True: + if result is True: self.refresh_data() def refresh_data(self): @@ -194,10 +195,6 @@ def refresh_data(self): _LOGGER.debug(climateResponse.__dict__) self.data[DATA_CLIMATE] = climateResponse.is_hvac_running - -""" -# Removing this block for now, as I do not have a Leaf with Nissan Connect to test it with. - if self.nissan_connect: try: locationResponse = self.get_location() @@ -214,8 +211,6 @@ def refresh_data(self): except Exception as e: _LOGGER.error("Error fetching location info") -""" - self.signal_components() def signal_components(self): @@ -266,7 +261,7 @@ def set_climate(self, toggle): self.signal_components() - return climate_result.is_hvac_running == False + return climate_result.is_hvac_running is False def get_location(self): request = self.leaf.request_location() diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 67ccc1b1ea98f..239d72fb603b1 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -5,15 +5,11 @@ """ import logging -from datetime import timedelta - -from homeassistant.components.sensor import ENTITY_ID_FORMAT from .. import nissan_leaf as LeafCore from ..nissan_leaf import LeafEntity from homeassistant.helpers.entity import Entity from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) -import asyncio from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM _LOGGER = logging.getLogger(__name__) @@ -61,7 +57,7 @@ def __init__(self, car, ac_on): @property def name(self): - if self.ac_on == True: + if self.ac_on is True: return self.car.leaf.nickname + " Range (AC)" else: return self.car.leaf.nickname + " Range" @@ -74,19 +70,19 @@ def log_registration(self): def state(self): ret = 0 - if self.ac_on == True: + if self.ac_on is True: ret = self.car.data[LeafCore.DATA_RANGE_AC] else: ret = self.car.data[LeafCore.DATA_RANGE_AC_OFF] - if self.car.hass.config.units.is_metric == False or self.car.config[LeafCore.DOMAIN][LeafCore.CONF_FORCE_MILES] == True: + if self.car.hass.config.units.is_metric is False or self.car.config[LeafCore.DOMAIN][LeafCore.CONF_FORCE_MILES] is True: ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) return round(ret, 0) @property def unit_of_measurement(self): - if self.car.hass.config.units.is_metric == False or self.car.config[LeafCore.DOMAIN][LeafCore.CONF_FORCE_MILES] == True: + if self.car.hass.config.units.is_metric is False or self.car.config[LeafCore.DOMAIN][LeafCore.CONF_FORCE_MILES] is True: return "mi" else: return "km" diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index fd1b97ab109de..80525849ce606 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -5,14 +5,8 @@ """ import logging -from datetime import timedelta - -from homeassistant.components.sensor import ENTITY_ID_FORMAT from .. import nissan_leaf as LeafCore from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) -import asyncio _LOGGER = logging.getLogger(__name__) @@ -41,7 +35,7 @@ def log_registration(self): @property def is_on(self): - return self.car.data[LeafCore.DATA_CLIMATE] == True + return self.car.data[LeafCore.DATA_CLIMATE] is True def turn_on(self, **kwargs): if self.car.set_climate(True): @@ -67,7 +61,7 @@ def log_registration(self): @property def is_on(self): - return self.car.data[LeafCore.DATA_CHARGING] == True + return self.car.data[LeafCore.DATA_CHARGING] is True def turn_on(self, **kwargs): if self.car.start_charging(): From 732fbe48488f4ad3cb4b1766adcb058115e78f1c Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Thu, 1 Feb 2018 16:45:18 +0000 Subject: [PATCH 13/78] Style fixes --- .../components/binary_sensor/nissan_leaf.py | 11 +- homeassistant/components/nissan_leaf.py | 121 +++++++++++------- .../components/sensor/nissan_leaf.py | 23 ++-- .../components/switch/nissan_leaf.py | 16 ++- 4 files changed, 100 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/binary_sensor/nissan_leaf.py b/homeassistant/components/binary_sensor/nissan_leaf.py index ff89182062259..4c39e591b6b00 100644 --- a/homeassistant/components/binary_sensor/nissan_leaf.py +++ b/homeassistant/components/binary_sensor/nissan_leaf.py @@ -1,15 +1,13 @@ """ -Plugged In Status Support for the Nissan Leaf Carwings/Nissan Connect API. +Plugged In Status Support for the Nissan Leaf -Documentation pending, please refer to the main platform component for configuration details +Documentation pending. +Please refer to the main platform component for configuration details """ import logging from .. import nissan_leaf as LeafCore -from ..nissan_leaf import LeafEntity -from homeassistant.helpers.entity import Entity -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM _LOGGER = logging.getLogger(__name__) @@ -36,7 +34,8 @@ def name(self): def log_registration(self): _LOGGER.debug( - "Registered LeafPluggedInSensor component with HASS for VIN " + self.car.leaf.vin) + "Registered LeafPluggedInSensor component with HASS for VIN %s", + self.car.leaf.vin) @property def state(self): diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 378f75b27784a..1f5769ade9834 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -2,9 +2,9 @@ Support for the Nissan Leaf Carwings/Nissan Connect API. Please note this is the pre-2018 API, which is still functional in the US. -The old API should continue to work as the new one is not being released outside the US. +The old API should continue to work for the forseeable future. -Documentation pages have not been created yet, here is an example configuration block: +Documentation has not been created yet, here is an example configuration block: nissan_leaf: username: "username" @@ -17,11 +17,13 @@ force_miles: true """ -import voluptuous as vol import logging from datetime import timedelta, datetime import time import urllib +import asyncio +import sys +import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD @@ -29,11 +31,10 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) from homeassistant.helpers.entity import Entity -import asyncio -import sys from homeassistant.helpers.event import track_time_interval -# Currently waiting on the pycarwings2 author to pull this PR in and update pip, so using my fork for now. +# Currently waiting on the lib author to review a PR +# So using my fork for now. # https://github.com/jdhorne/pycarwings2/pull/28 REQUIREMENTS = ['https://github.com/BenWoodford/pycarwings2/archive/master.zip' @@ -72,8 +73,10 @@ vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS), vol.Optional(CONF_NCONNECT, default=True): cv.boolean, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, - vol.Optional(CONF_CHARGING_INTERVAL, default=DEFAULT_CHARGING_INTERVAL): cv.positive_int, - vol.Optional(CONF_CLIMATE_INTERVAL, default=DEFAULT_CLIMATE_INTERVAL): cv.positive_int, + vol.Optional(CONF_CHARGING_INTERVAL, + default=DEFAULT_CHARGING_INTERVAL): cv.positive_int, + vol.Optional(CONF_CLIMATE_INTERVAL, + default=DEFAULT_CLIMATE_INTERVAL): cv.positive_int, vol.Optional(CONF_FORCE_MILES, default=False): cv.boolean }) }, extra=vol.ALLOW_EXTRA) @@ -102,27 +105,32 @@ def setup(hass, config): _LOGGER.error( "Unable to connect to Nissan Connect with username and password") return False - except(KeyError): + except KeyError: _LOGGER.error( - "Unable to fetch car details... do you actually have a Leaf connected to your account?") + "Unable to fetch car details..." + " do you actually have a Leaf connected to your account?") return False except: _LOGGER.error( - "An unknown error occurred while connecting to Nissan's servers: ", sys.exc_info()[0]) + "An unknown error occurred while connecting to Nissan: %s", + sys.exc_info()[0]) _LOGGER.info("Successfully logged in and fetched Leaf info") - _LOGGER.info("WARNING: This component may poll your Leaf too often, and drain the 12V. If you drain your car's 12V it won't start as the drive train battery won't connect, so you have been warned.") + _LOGGER.info( + "WARNING: This may poll your Leaf too often, and drain the 12V." + " If you drain your car's 12V it won't start" + " as the drive train battery won't connect" + " Don't set the intervals too low.") hass.data[DATA_LEAF] = {} hass.data[DATA_LEAF][leaf.vin] = LeafDataStore( leaf, hass, config) for component in LEAF_COMPONENTS: - if (component != 'device_tracker') or (config[DOMAIN][CONF_NCONNECT] is True): + if (component != 'device_tracker' or + config[DOMAIN][CONF_NCONNECT] is True): load_platform(hass, component, DOMAIN, {}, config) - # hass.data[DATA_LEAF][leaf.vin].refresh_leaf_if_necessary(0) - return True @@ -131,8 +139,9 @@ class LeafDataStore: def __init__(self, leaf, hass, config): self.leaf = leaf self.config = config - #self.nissan_connect = config[DOMAIN][CONF_NCONNECT] + # self.nissan_connect = config[DOMAIN][CONF_NCONNECT] self.nissan_connect = False # Disabled until tested and implemented + self.force_miles = config[DOMAIN][CONF_FORCE_MILES] self.hass = hass self.data = {} self.data[DATA_CLIMATE] = False @@ -142,7 +151,7 @@ def __init__(self, leaf, hass, config): self.data[DATA_RANGE_AC] = 0 self.data[DATA_RANGE_AC_OFF] = 0 self.data[DATA_PLUGGED_IN] = False - self.lastCheck = None + self.last_check = None track_time_interval( hass, self.refresh_leaf_if_necessary, timedelta(seconds=CHECK_INTERVAL)) @@ -150,21 +159,35 @@ def refresh_leaf_if_necessary(self, event_time): result = False now = datetime.today() - if self.lastCheck is None: - _LOGGER.debug("Firing Refresh on " + self.leaf.vin + - " as there has not been one yet.") + base_interval = timedelta(minutes=self.config[DOMAIN][CONF_INTERVAL]) + climate_interval = timedelta( + minutes=self.config[DOMAIN][CONF_CLIMATE_INTERVAL]) + charging_interval = timedelta( + minutes=self.config[DOMAIN][CONF_CHARGING_INTERVAL]) + + if self.last_check is None: + _LOGGER.debug("Firing Refresh on %s" + " as there has not been one yet.", + self.leaf.nickname) result = True - elif self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_INTERVAL]) < now: - _LOGGER.debug("Firing Refresh on " + self.leaf.vin + - " as the interval has passed.") + elif self.last_check + base_interval < now: + _LOGGER.debug("Firing Refresh on %s" + " as the interval has passed.", + self.leaf.nickname) result = True - elif self.data[DATA_CHARGING] is True and self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_CHARGING_INTERVAL]) < now: - _LOGGER.debug("Firing Refresh on " + self.leaf.vin + - " as it's charging and the charging interval has passed.") + elif (self.data[DATA_CHARGING] is True and + self.last_check + charging_interval < now): + _LOGGER.debug("Firing Refresh on %s " + " as it's charging and the charging interval" + " has passed.", + self.leaf.nickname) result = True - elif self.data[DATA_CLIMATE] is True and self.lastCheck + timedelta(minutes=self.config[DOMAIN][CONF_CLIMATE_INTERVAL]) < now: - _LOGGER.debug("Firing Refresh on " + self.leaf.vin + - " as climate control is on and the interval has passed.") + elif (self.data[DATA_CLIMATE] is True and + self.last_check + climate_interval < now): + _LOGGER.debug("Firing Refresh on %s" + " as climate control is on and" + " the interval has passed.", + self.leaf.nickname) result = True if result is True: @@ -173,42 +196,44 @@ def refresh_leaf_if_necessary(self, event_time): def refresh_data(self): _LOGGER.debug("Updating Nissan Leaf Data") - self.lastCheck = datetime.today() + self.last_check = datetime.today() - batteryResponse = self.get_battery() + battery_response = self.get_battery() _LOGGER.debug("Got battery data for Leaf") - if batteryResponse.answer['status'] == 200: - self.data[DATA_BATTERY] = batteryResponse.battery_percent - self.data[DATA_CHARGING] = batteryResponse.is_charging - self.data[DATA_PLUGGED_IN] = batteryResponse.is_connected - self.data[DATA_RANGE_AC] = batteryResponse.cruising_range_ac_on_km - self.data[DATA_RANGE_AC_OFF] = batteryResponse.cruising_range_ac_off_km + if battery_response.answer['status'] == 200: + self.data[DATA_BATTERY] = battery_response.battery_percent + self.data[DATA_CHARGING] = battery_response.is_charging + self.data[DATA_PLUGGED_IN] = battery_response.is_connected + self.data[DATA_RANGE_AC] = battery_response.cruising_range_ac_on_km + self.data[DATA_RANGE_AC_OFF] = ( + battery_response.cruising_range_ac_off_km + ) _LOGGER.debug("Battery Response: ") - _LOGGER.debug(batteryResponse.__dict__) + _LOGGER.debug(battery_response.__dict__) - climateResponse = self.get_climate() + climate_response = self.get_climate() - if climateResponse is not None: + if climate_response is not None: _LOGGER.debug("Got climate data for Leaf") - _LOGGER.debug(climateResponse.__dict__) - self.data[DATA_CLIMATE] = climateResponse.is_hvac_running + _LOGGER.debug(climate_response.__dict__) + self.data[DATA_CLIMATE] = climate_response.is_hvac_running if self.nissan_connect: try: - locationResponse = self.get_location() + location_response = self.get_location() - if locationResponse is None: + if location_response is None: _LOGGER.debug("Empty Location Response Received") self.data[DATA_LOCATION] = None else: - LOGGER.debug("Got location data for Leaf") - self.data[DATA_LOCATION] = locationResponse + _LOGGER.debug("Got location data for Leaf") + self.data[DATA_LOCATION] = location_response _LOGGER.debug("Location Response: ") - _LOGGER.debug(locationResponse.__dict__) - except Exception as e: + _LOGGER.debug(location_response.__dict__) + except: _LOGGER.error("Error fetching location info") self.signal_components() diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 239d72fb603b1..b0d52db81f4a8 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -1,16 +1,13 @@ """ -Battery Charge and Range Support for the Nissan Leaf Carwings/Nissan Connect API. +Battery Charge and Range Support for the Nissan Leaf -Documentation pending, please refer to the main platform component for configuration details +Documentation pending. +Please refer to the main platform component for configuration details """ import logging -from .. import nissan_leaf as LeafCore -from ..nissan_leaf import LeafEntity -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from .. import nissan_leaf as LeafCore _LOGGER = logging.getLogger(__name__) @@ -39,7 +36,8 @@ def name(self): def log_registration(self): _LOGGER.debug( - "Registered LeafBatterySensor component with HASS for VIN " + self.car.leaf.vin) + "Registered LeafBatterySensor component with HASS for VIN %s", + self.car.leaf.vin) @property def state(self): @@ -64,7 +62,8 @@ def name(self): def log_registration(self): _LOGGER.debug( - "Registered LeafRangeSensor component with HASS for VIN " + self.car.leaf.vin) + "Registered LeafRangeSensor component with HASS for VIN %s", + self.car.leaf.vin) @property def state(self): @@ -75,14 +74,16 @@ def state(self): else: ret = self.car.data[LeafCore.DATA_RANGE_AC_OFF] - if self.car.hass.config.units.is_metric is False or self.car.config[LeafCore.DOMAIN][LeafCore.CONF_FORCE_MILES] is True: + if (self.car.hass.config.units.is_metric is False or + self.car.force_miles is True): ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) return round(ret, 0) @property def unit_of_measurement(self): - if self.car.hass.config.units.is_metric is False or self.car.config[LeafCore.DOMAIN][LeafCore.CONF_FORCE_MILES] is True: + if (self.car.hass.config.units.is_metric is False or + self.car.force_miles is True): return "mi" else: return "km" diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index 80525849ce606..105240a96bf64 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -1,12 +1,13 @@ """ -Charge and Climate Control Support for the Nissan Leaf Carwings/Nissan Connect API. +Charge and Climate Control Support for the Nissan Leaf -Documentation pending, please refer to the main platform component for configuration details +Documentation pending. +Please refer to the main platform component for configuration details """ import logging -from .. import nissan_leaf as LeafCore from homeassistant.helpers.entity import ToggleEntity +from .. import nissan_leaf as LeafCore _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,8 @@ def name(self): def log_registration(self): _LOGGER.debug( - "Registered LeafClimateSwitch component with HASS for VIN " + self.car.leaf.vin) + "Registered LeafClimateSwitch component with HASS for VIN %s", + self.car.leaf.vin) @property def is_on(self): @@ -57,7 +59,8 @@ def name(self): def log_registration(self): _LOGGER.debug( - "Registered LeafChargeSwitch component with HASS for VIN " + self.car.leaf.vin) + "Registered LeafChargeSwitch component with HASS for VIN %s", + self.car.leaf.vin) @property def is_on(self): @@ -71,4 +74,5 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): _LOGGER.debug( - "Cannot turn off Leaf charging - Nissan does not support that remotely.") + "Cannot turn off Leaf charging -" + " Nissan does not support that remotely.") From e446aa4ba86daa0c1a4060e84786f5f9d8d94b46 Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Thu, 1 Feb 2018 16:47:11 +0000 Subject: [PATCH 14/78] Fixing the final line length --- homeassistant/components/nissan_leaf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 1f5769ade9834..5d701ff438d83 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -153,7 +153,9 @@ def __init__(self, leaf, hass, config): self.data[DATA_PLUGGED_IN] = False self.last_check = None track_time_interval( - hass, self.refresh_leaf_if_necessary, timedelta(seconds=CHECK_INTERVAL)) + hass, + self.refresh_leaf_if_necessary, + timedelta(seconds=CHECK_INTERVAL)) def refresh_leaf_if_necessary(self, event_time): result = False @@ -324,5 +326,4 @@ def async_added_to_hass(self): def _update_callback(self): """Callback update method.""" - #_LOGGER.debug("Got dispatcher update from Leaf platform") self.schedule_update_ha_state(True) From 053c38324f5b7be835d7fa142ffe50b3b4ab8b4c Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Tue, 6 Feb 2018 15:21:34 +0000 Subject: [PATCH 15/78] Fixed an issue with newer models and bad climate data --- homeassistant/components/nissan_leaf.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 5d701ff438d83..dbeb8ae98cdd2 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -204,7 +204,11 @@ def refresh_data(self): _LOGGER.debug("Got battery data for Leaf") if battery_response.answer['status'] == 200: - self.data[DATA_BATTERY] = battery_response.battery_percent + if int(battery_response.battery_capacity) > 100: + self.data[DATA_BATTERY] = battery_response.battery_percent * 0.05 + else: + self.data[DATA_BATTERY] = battery_response.battery_percent + self.data[DATA_CHARGING] = battery_response.is_charging self.data[DATA_PLUGGED_IN] = battery_response.is_connected self.data[DATA_RANGE_AC] = battery_response.cruising_range_ac_on_km @@ -254,8 +258,11 @@ def get_battery(self): return battery_status def get_climate(self): - request = self.leaf.get_latest_hvac_status() - return request + try: + request = self.leaf.get_latest_hvac_status() + return request + except TypeError: + return None def set_climate(self, toggle): if toggle: From a5d698d6b35737bdacf01d6fd79668a5be5a92f5 Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Tue, 6 Feb 2018 15:30:03 +0000 Subject: [PATCH 16/78] Forgot to check my line endings. --- homeassistant/components/nissan_leaf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index dbeb8ae98cdd2..dc572098de27d 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -205,7 +205,9 @@ def refresh_data(self): if battery_response.answer['status'] == 200: if int(battery_response.battery_capacity) > 100: - self.data[DATA_BATTERY] = battery_response.battery_percent * 0.05 + self.data[DATA_BATTERY] = ( + battery_response.battery_percent * 0.05 + ) else: self.data[DATA_BATTERY] = battery_response.battery_percent From 170fa1a451cfd789a4a6842b020dc403fab2e184 Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Thu, 15 Feb 2018 15:05:31 +0000 Subject: [PATCH 17/78] New icons for most of the components --- .../components/binary_sensor/nissan_leaf.py | 7 +++++++ homeassistant/components/sensor/nissan_leaf.py | 13 +++++++++++++ homeassistant/components/switch/nissan_leaf.py | 14 ++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/homeassistant/components/binary_sensor/nissan_leaf.py b/homeassistant/components/binary_sensor/nissan_leaf.py index 4c39e591b6b00..62eb80007abb3 100644 --- a/homeassistant/components/binary_sensor/nissan_leaf.py +++ b/homeassistant/components/binary_sensor/nissan_leaf.py @@ -40,3 +40,10 @@ def log_registration(self): @property def state(self): return self.car.data[LeafCore.DATA_PLUGGED_IN] + + @property + def icon(self): + if self.car.data[LeafCore.DATA_PLUGGED_IN]: + return 'mdi:power-plug' + else: + return 'mdi:power-plug-off' diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index b0d52db81f4a8..46ee1fefa6dcc 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -7,6 +7,7 @@ import logging from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.helpers.icon import icon_for_battery_level from .. import nissan_leaf as LeafCore _LOGGER = logging.getLogger(__name__) @@ -47,6 +48,14 @@ def state(self): def unit_of_measurement(self): return '%' + @property + def icon(self): + chargeState = self.car.data[LeafCore.DATA_CHARGING] + return icon_for_battery_level( + battery_level=self.state, + charging=chargeState + ) + class LeafRangeSensor(LeafCore.LeafEntity): def __init__(self, car, ac_on): @@ -87,3 +96,7 @@ def unit_of_measurement(self): return "mi" else: return "km" + + @property + def icon(self): + return 'mdi:speedometer' diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index 105240a96bf64..91e73658f21bc 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -51,6 +51,13 @@ def turn_off(self, **kwargs): self._update_callback() + @property + def icon(self): + if self.car.data[LeafCore.DATA_CLIMATE]: + return 'mdi:fan' + else: + return 'mdi:fan-off' + class LeafChargeSwitch(LeafCore.LeafEntity, ToggleEntity): @property @@ -62,6 +69,13 @@ def log_registration(self): "Registered LeafChargeSwitch component with HASS for VIN %s", self.car.leaf.vin) + @property + def icon(self): + if self.car.data[LeafCore.DATA_CHARGING]: + return 'mdi:flash' + else: + return 'mdi:flash-off' + @property def is_on(self): return self.car.data[LeafCore.DATA_CHARGING] is True From 1693701e6714aaa1392be88d246d1578dac87c2c Mon Sep 17 00:00:00 2001 From: Ben Woodford Date: Thu, 27 Dec 2018 17:16:30 +0000 Subject: [PATCH 18/78] Hotfix for handling Nissan's awful servers --- homeassistant/components/nissan_leaf.py | 69 ++++++++++++++++++------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index dc572098de27d..4a39dca9ab46d 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -58,11 +58,13 @@ CONF_CHARGING_INTERVAL = 'update_interval_charging' CONF_CLIMATE_INTERVAL = 'update_interval_climate' CONF_REGION = 'region' +CONF_REFRESH_ATTEMPTS = 'refresh_attempts' CONF_VALID_REGIONS = ['NNA', 'NE', 'NCI', 'NMA', 'NML'] CONF_FORCE_MILES = 'force_miles' DEFAULT_INTERVAL = 30 DEFAULT_CHARGING_INTERVAL = 15 DEFAULT_CLIMATE_INTERVAL = 5 +DEFAULT_REFRESH_ATTEMPTS = 4 CHECK_INTERVAL = 10 @@ -77,7 +79,8 @@ default=DEFAULT_CHARGING_INTERVAL): cv.positive_int, vol.Optional(CONF_CLIMATE_INTERVAL, default=DEFAULT_CLIMATE_INTERVAL): cv.positive_int, - vol.Optional(CONF_FORCE_MILES, default=False): cv.boolean + vol.Optional(CONF_FORCE_MILES, default=False): cv.boolean, + vol.Optional(CONF_REFRESH_ATTEMPTS, default=DEFAULT_REFRESH_ATTEMPTS): cv.positive_int }) }, extra=vol.ALLOW_EXTRA) @@ -203,23 +206,21 @@ def refresh_data(self): battery_response = self.get_battery() _LOGGER.debug("Got battery data for Leaf") - if battery_response.answer['status'] == 200: - if int(battery_response.battery_capacity) > 100: - self.data[DATA_BATTERY] = ( - battery_response.battery_percent * 0.05 - ) - else: - self.data[DATA_BATTERY] = battery_response.battery_percent - - self.data[DATA_CHARGING] = battery_response.is_charging - self.data[DATA_PLUGGED_IN] = battery_response.is_connected - self.data[DATA_RANGE_AC] = battery_response.cruising_range_ac_on_km - self.data[DATA_RANGE_AC_OFF] = ( - battery_response.cruising_range_ac_off_km - ) + if battery_response is not None: + _LOGGER.debug("Battery Response: ") + _LOGGER.debug(battery_response.__dict__) + if battery_response.answer['status'] == 200: + if int(battery_response.battery_capacity) > 100: + self.data[DATA_BATTERY] = battery_response.battery_percent * 0.05 + else: + self.data[DATA_BATTERY] = battery_response.battery_percent - _LOGGER.debug("Battery Response: ") - _LOGGER.debug(battery_response.__dict__) + self.data[DATA_CHARGING] = battery_response.is_charging + self.data[DATA_PLUGGED_IN] = battery_response.is_connected + self.data[DATA_RANGE_AC] = battery_response.cruising_range_ac_on_km + self.data[DATA_RANGE_AC_OFF] = ( + battery_response.cruising_range_ac_off_km + ) climate_response = self.get_climate() @@ -252,8 +253,18 @@ def signal_components(self): def get_battery(self): request = self.leaf.request_update() battery_status = self.leaf.get_status_from_update(request) + + i = 0 + while battery_status is None: - _LOGGER.debug("Battery data not in yet.") + if i >= self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]: + _LOGGER.debug("Climate Data failed to arrive within %d attempts", + self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]) + break + + i += 1 + + _LOGGER.debug("Battery data not in yet (Attempt %d)", i) time.sleep(5) battery_status = self.leaf.get_status_from_update(request) @@ -272,8 +283,17 @@ def set_climate(self, toggle): climate_result = self.leaf.get_start_climate_control_result( request) + i = 0 + while climate_result is None: - _LOGGER.debug("Climate data not in yet.") + if i >= self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]: + _LOGGER.debug("Climate Data failed to arrive within %d attempts", + self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]) + break + + i += 1 + + _LOGGER.debug("Climate data not in yet (Attempt %d)", i) time.sleep(5) climate_result = self.leaf.get_start_climate_control_result( request) @@ -287,8 +307,17 @@ def set_climate(self, toggle): request = self.leaf.stop_climate_control() climate_result = self.leaf.get_stop_climate_control_result(request) + i = 0 + while climate_result is None: - _LOGGER.debug("Climate data not in yet.") + if i >= self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]: + _LOGGER.debug("Climate Data failed to arrive within %d attempts", + self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]) + break + + i += 1 + + _LOGGER.debug("Climate data not in yet (Attempt %d)", i) time.sleep(5) climate_result = self.leaf.get_stop_climate_control_result( request) From c3c572f4922a15d49bf1cf92821ed3bd408f1ca3 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 07:32:17 +0000 Subject: [PATCH 19/78] Merge in fixes made by Phil Cole Remove invalid FIXMEs and update TODOs Fixes for pylint and test for CarwingsError exception rather than Exception Flake8 fixes Add pycarwings2 to requirements_all.txt Add extra configuration documentation. Use pycarwings2 from pip. Check server dates between requests. Add sensor device class for battery. Async conversion fixes flake8 fixes and docstrings Non-async charging is OK Handle multiple cars in the configuration Convert to async. Better imports for platforms Fix scanning interval & prevent extra refreshes. async switchover Check discovery_info to prevent load of platforms Ensure update frequency is always above a minimum interval (1 min). Platforms don't have return values Use values() instead of items() when not using key Use snake_case (LeafCore becomes leaf_core) commit 418b6bbcc49cf2909aac85869440435410abf3fd --- .../components/binary_sensor/nissan_leaf.py | 31 +- .../components/device_tracker/nissan_leaf.py | 46 ++ homeassistant/components/nissan_leaf.py | 567 +++++++++++------- .../components/sensor/nissan_leaf.py | 58 +- .../components/switch/nissan_leaf.py | 69 ++- 5 files changed, 513 insertions(+), 258 deletions(-) create mode 100644 homeassistant/components/device_tracker/nissan_leaf.py diff --git a/homeassistant/components/binary_sensor/nissan_leaf.py b/homeassistant/components/binary_sensor/nissan_leaf.py index 51ef46185d358..642c4dd5d2265 100644 --- a/homeassistant/components/binary_sensor/nissan_leaf.py +++ b/homeassistant/components/binary_sensor/nissan_leaf.py @@ -1,5 +1,5 @@ """ -Plugged In Status Support for the Nissan Leaf +Plugged In Status Support for the Nissan Leaf. Documentation pending. Please refer to the main platform component for configuration details @@ -7,7 +7,8 @@ import logging -from .. import nissan_leaf as LeafCore +from homeassistant.components.nissan_leaf import ( + DATA_LEAF, DATA_PLUGGED_IN, LeafEntity) _LOGGER = logging.getLogger(__name__) @@ -15,35 +16,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - devices = [] - - _LOGGER.debug("Adding sensors") + """Nissan Leaf binary_sensor setup.""" + _LOGGER.debug("binary_sensor setup_platform, discovery_info=%s", + discovery_info) - for key, value in hass.data[LeafCore.DATA_LEAF].items(): + 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)) add_devices(devices, True) - return True +class LeafPluggedInSensor(LeafEntity): + """Plugged In Sensor class.""" -class LeafPluggedInSensor(LeafCore.LeafEntity): @property def name(self): + """Sensor name.""" return self.car.leaf.nickname + " Plug Status" def log_registration(self): + """Log registration.""" _LOGGER.debug( "Registered LeafPluggedInSensor component with HASS for VIN %s", self.car.leaf.vin) @property def state(self): - return self.car.data[LeafCore.DATA_PLUGGED_IN] + """Return true if plugged in.""" + return self.car.data[DATA_PLUGGED_IN] @property def icon(self): - if self.car.data[LeafCore.DATA_PLUGGED_IN]: + """Icon handling.""" + if self.car.data[DATA_PLUGGED_IN]: return 'mdi:power-plug' - else: - return 'mdi:power-plug-off' + return 'mdi:power-plug-off' diff --git a/homeassistant/components/device_tracker/nissan_leaf.py b/homeassistant/components/device_tracker/nissan_leaf.py new file mode 100644 index 0000000000000..6b797ecb49071 --- /dev/null +++ b/homeassistant/components/device_tracker/nissan_leaf.py @@ -0,0 +1,46 @@ +""" +Support for tracking a Nissan Leaf. + +For more details about this platform, please refer to the documentation +of the main platform component. +""" +import logging +from homeassistant.util import slugify +from homeassistant.helpers.dispatcher import ( + dispatcher_connect, dispatcher_send) +from homeassistant.components.nissan_leaf import ( + DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF +) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + + +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) + + def see_vehicle(): + """Handle the reporting of the vehicle position.""" + for key, value in hass.data[DATA_LEAF].items(): + host_name = value.leaf.nickname + dev_id = 'nissan_leaf_{}'.format(slugify(host_name)) + if value.data[DATA_LOCATION] in [None, False]: + _LOGGER.debug("No position found for vehicle %s", key) + return False + _LOGGER.debug("Updating device_tracker for %s with position %s", + value.leaf.nickname, value.data[DATA_LOCATION]) + see(dev_id=dev_id, + host_name=host_name, + gps=( + value.data[DATA_LOCATION].latitude, + value.data[DATA_LOCATION].longitude + ), + icon='mdi:car') + + dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) + dispatcher_send(hass, SIGNAL_UPDATE_LEAF) + + return True diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 7e1bcf5cc7807..2d74520fba110 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -7,19 +7,53 @@ Documentation has not been created yet, here is an example configuration block: nissan_leaf: - username: "username" - password: "password" - nissan_connect: false - region: 'NE' - update_interval: 30 - update_interval_charging: 15 - update_interval_climate: 5 - force_miles: true + - username: "username" + password: "password" + nissan_connect: true + region: 'NE' + update_interval: + hours: 1 + update_interval_charging: + minutes: 15 + update_interval_climate: + minutes: 5 + force_miles: true + + +Notes for the above: + +region: Must be one of + 'NE' (Europe), + 'NNA' (US), + 'NCI' (Canada), + 'NMA' (Australia), + 'NML' (Japan) +nissan_connect: If your car has the updated head unit (Nissan Connect rather + than Car Wings) then you can pull the location, shown in a + device tracker. If you have a pre-2016 24kWh Leaf then you + will have CarWings and should set this to false, or it will + crash the component. +update_interval: The interval between updates if AC is off and not charging +update_interval_charging: The interval between updates if charging +update_interval_climate: The interval between updates if climate control is on + +Notes for testers: + +Please report bugs using the following logger config. + +logger: + default: critical + logs: + homeassistant.components.nissan_leaf: debug + homeassistant.components.sensor.nissan_leaf: debug + homeassistant.components.switch.nissan_leaf: debug + homeassistant.components.device_tracker.nissan_leaf: debug + + """ import logging from datetime import timedelta, datetime -import time import urllib import asyncio import sys @@ -29,16 +63,26 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) + async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval - -# Currently waiting on the lib author to review a PR -# So using my fork for now. -# https://github.com/jdhorne/pycarwings2/pull/28 - -REQUIREMENTS = ['https://github.com/BenWoodford/pycarwings2/archive/master.zip' - '#pycarwings'] +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + +# TODO: Disable charging switch if currently charging, because charging +# cannot be stopped remotely by the Nissan APIs. + +# If testing then use the following kinds of URLs +# +# REQUIREMENTS = ['https://github.com/filcole/pycarwings2/archive/master.zip' +# '#pycarwings2'] +# REQUIREMENTS = ['https://test-files.pythonhosted.org/packages/7c/ad/ +# ee27988357f1710ca9ced1a60263486415f054003ca6fa396922ca6b6bbf/ +# pycarwings2-2.2.tar.gz' +# '#pycarwings2'] +# REQUIREMENTS = ['file:///home/phil/repos/pycarwings2ve/pycarwings2/ +# dist/pycarwings2-2.2.tar.gz' +# '#pycarwings2'] +REQUIREMENTS = ['pycarwings2==2.2'] _LOGGER = logging.getLogger(__name__) @@ -58,93 +102,119 @@ CONF_CHARGING_INTERVAL = 'update_interval_charging' CONF_CLIMATE_INTERVAL = 'update_interval_climate' CONF_REGION = 'region' -CONF_REFRESH_ATTEMPTS = 'refresh_attempts' CONF_VALID_REGIONS = ['NNA', 'NE', 'NCI', 'NMA', 'NML'] CONF_FORCE_MILES = 'force_miles' -DEFAULT_INTERVAL = 30 -DEFAULT_CHARGING_INTERVAL = 15 -DEFAULT_CLIMATE_INTERVAL = 5 -DEFAULT_REFRESH_ATTEMPTS = 4 -CHECK_INTERVAL = 10 +MIN_UPDATE_INTERVAL = timedelta(minutes=2) +DEFAULT_INTERVAL = timedelta(minutes=30) +DEFAULT_CHARGING_INTERVAL = timedelta(minutes=15) +DEFAULT_CLIMATE_INTERVAL = timedelta(minutes=5) +RESTRICTED_BATTERY = 2 +RESTRICTED_INTERVAL = timedelta(hours=12) + +MAX_RESPONSE_ATTEMPTS = 22 CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS), vol.Optional(CONF_NCONNECT, default=True): cv.boolean, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): ( + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))), vol.Optional(CONF_CHARGING_INTERVAL, - default=DEFAULT_CHARGING_INTERVAL): cv.positive_int, + default=DEFAULT_CHARGING_INTERVAL): ( + vol.All(cv.time_period, + vol.Clamp( + min=MIN_UPDATE_INTERVAL))), vol.Optional(CONF_CLIMATE_INTERVAL, - default=DEFAULT_CLIMATE_INTERVAL): cv.positive_int, - vol.Optional(CONF_FORCE_MILES, default=False): cv.boolean, - vol.Optional(CONF_REFRESH_ATTEMPTS, default=DEFAULT_REFRESH_ATTEMPTS): cv.positive_int - }) + default=DEFAULT_CLIMATE_INTERVAL): ( + vol.All(cv.time_period, + vol.Clamp( + min=MIN_UPDATE_INTERVAL))), + vol.Optional(CONF_FORCE_MILES, default=False): cv.boolean + })]) }, extra=vol.ALLOW_EXTRA) LEAF_COMPONENTS = [ - 'sensor', 'switch', 'binary_sensor' + 'sensor', 'switch', 'binary_sensor', 'device_tracker' ] SIGNAL_UPDATE_LEAF = 'nissan_leaf_update' -def setup(hass, config): - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] - +@asyncio.coroutine +def async_setup(hass, config): + """Setup the nissan leaf component.""" import pycarwings2 - _LOGGER.debug("Logging into You+Nissan...") + async def async_setup_leaf(car_config): + """Setup a leaf car.""" + _LOGGER.debug("Logging into You+Nissan...") - try: - s = pycarwings2.Session( - username, password, config[DOMAIN][CONF_REGION]) - _LOGGER.debug("Fetching Leaf Data") - leaf = s.get_leaf() - except(RuntimeError, urllib.error.HTTPError): - _LOGGER.error( - "Unable to connect to Nissan Connect with username and password") - return False - except KeyError: - _LOGGER.error( - "Unable to fetch car details..." - " do you actually have a Leaf connected to your account?") - return False - except: - _LOGGER.error( - "An unknown error occurred while connecting to Nissan: %s", - sys.exc_info()[0]) - - _LOGGER.info("Successfully logged in and fetched Leaf info") - _LOGGER.info( - "WARNING: This may poll your Leaf too often, and drain the 12V." - " If you drain your car's 12V it won't start" - " as the drive train battery won't connect" - " Don't set the intervals too low.") + username = car_config[CONF_USERNAME] + password = car_config[CONF_PASSWORD] + region = car_config[CONF_REGION] + leaf = None - hass.data[DATA_LEAF] = {} - hass.data[DATA_LEAF][leaf.vin] = LeafDataStore( - leaf, hass, config) + 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 + except KeyError: + _LOGGER.error( + "Unable to fetch car details..." + " do you actually have a Leaf connected to your account?") + return False + except pycarwings2.CarwingsError: + _LOGGER.error( + "An unknown error occurred while connecting to Nissan: %s", + sys.exc_info()[0]) + return False + + _LOGGER.info("Successfully logged in and fetched Leaf info") + _LOGGER.info( + "WARNING: This may poll your Leaf too often, and drain the 12V" + " battery. If you drain your cars 12V battery it WILL NOT START" + " as the drive train battery won't connect." + " Don't set the intervals too low.") + + data_store = LeafDataStore(leaf, hass, car_config) + hass.data[DATA_LEAF][leaf.vin] = data_store + await hass.async_add_job(data_store.async_update_data, None) + + for component in LEAF_COMPONENTS: + if (component != 'device_tracker' or + car_config[CONF_NCONNECT] is True): + load_platform(hass, component, DOMAIN, {}, car_config) - for component in LEAF_COMPONENTS: - if (component != 'device_tracker' or - config[DOMAIN][CONF_NCONNECT] is True): - load_platform(hass, component, DOMAIN, {}, config) + hass.data[DATA_LEAF] = {} + tasks = [async_setup_leaf(car) for car in config[DOMAIN]] + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) return True class LeafDataStore: + """Nissan Leaf Data Store.""" - def __init__(self, leaf, hass, config): + def __init__(self, leaf, hass, car_config): + """Initialise the data store.""" self.leaf = leaf - self.config = config - # self.nissan_connect = config[DOMAIN][CONF_NCONNECT] - self.nissan_connect = False # Disabled until tested and implemented - self.force_miles = config[DOMAIN][CONF_FORCE_MILES] + 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 @@ -155,83 +225,126 @@ def __init__(self, leaf, hass, config): self.data[DATA_RANGE_AC_OFF] = 0 self.data[DATA_PLUGGED_IN] = False self.last_check = None - track_time_interval( - hass, - self.refresh_leaf_if_necessary, - timedelta(seconds=CHECK_INTERVAL)) - - def refresh_leaf_if_necessary(self, event_time): - result = False - now = datetime.today() - - base_interval = timedelta(minutes=self.config[DOMAIN][CONF_INTERVAL]) - climate_interval = timedelta( - minutes=self.config[DOMAIN][CONF_CLIMATE_INTERVAL]) - charging_interval = timedelta( - minutes=self.config[DOMAIN][CONF_CHARGING_INTERVAL]) - - if self.last_check is None: - _LOGGER.debug("Firing Refresh on %s" - " as there has not been one yet.", - self.leaf.nickname) - result = True - elif self.last_check + base_interval < now: - _LOGGER.debug("Firing Refresh on %s" - " as the interval has passed.", - self.leaf.nickname) - result = True - elif (self.data[DATA_CHARGING] is True and - self.last_check + charging_interval < now): - _LOGGER.debug("Firing Refresh on %s " - " as it's charging and the charging interval" - " has passed.", - self.leaf.nickname) - result = True - elif (self.data[DATA_CLIMATE] is True and - self.last_check + climate_interval < now): - _LOGGER.debug("Firing Refresh on %s" - " as climate control is on and" - " the interval has passed.", - self.leaf.nickname) - result = True + self.last_battery_response = None + self.request_in_progress = False + + @asyncio.coroutine + async def async_update_data(self, now): + """Update data from nissan leaf.""" + await self.async_refresh_data() + next_interval = self.get_next_interval() + _LOGGER.debug("Next interval=%s", next_interval) + async_track_point_in_utc_time( + self.hass, self.async_update_data, next_interval) + + def get_next_interval(self): + """Calculate when the next update should occur.""" + base_interval = self.car_config[CONF_INTERVAL] + climate_interval = self.car_config[CONF_CLIMATE_INTERVAL] + charging_interval = self.car_config[CONF_CHARGING_INTERVAL] + + # The 12V battery is used when communicating with Nissan servers. + # The 12V battery is charged from the traction battery when not + # connected # and when the traction battery has enough charge. To + # avoid draining the 12V battery we shall restrict the update + # frequency if low battery detected. + 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) + 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 - if result is True: - self.refresh_data() + async def async_refresh_data(self): + """Refresh the leaf data and update the datastore.""" + from pycarwings2 import CarwingsError + + if self.request_in_progress: + _LOGGER.debug("Refresh currently in progress for %s", + self.leaf.nickname) + return - def refresh_data(self): _LOGGER.debug("Updating Nissan Leaf Data") self.last_check = datetime.today() + self.request_in_progress = True - battery_response = self.get_battery() - _LOGGER.debug("Got battery data for Leaf") + (battery_response, server_response) = await self.async_get_battery() if battery_response is not None: _LOGGER.debug("Battery Response: ") _LOGGER.debug(battery_response.__dict__) + if battery_response.answer['status'] == 200: if int(battery_response.battery_capacity) > 100: - self.data[DATA_BATTERY] = battery_response.battery_percent * 0.05 + self.data[DATA_BATTERY] = ( + battery_response.battery_percent * 0.05 + ) else: self.data[DATA_BATTERY] = battery_response.battery_percent self.data[DATA_CHARGING] = battery_response.is_charging self.data[DATA_PLUGGED_IN] = battery_response.is_connected - self.data[DATA_RANGE_AC] = battery_response.cruising_range_ac_on_km + self.data[DATA_RANGE_AC] = ( + battery_response.cruising_range_ac_on_km + ) self.data[DATA_RANGE_AC_OFF] = ( battery_response.cruising_range_ac_off_km ) + self.signal_components() + self.last_battery_response = utcnow() + + if server_response is not None: + _LOGGER.debug("Server Response: ") + _LOGGER.debug(server_response.__dict__) + + if server_response.answer['status'] == 200: + if server_response.state_of_charge is not None: + self.data[DATA_BATTERY] = int( + server_response.state_of_charge + ) + else: + self.data[DATA_BATTERY] = server_response.battery_percent + + self.data[DATA_RANGE_AC] = ( + server_response.cruising_range_ac_on_km + ) + self.data[DATA_RANGE_AC_OFF] = ( + server_response.cruising_range_ac_off_km + ) + self.signal_components() + self.last_battery_response = utcnow() - climate_response = self.get_climate() + # Climate response only updated if battery data updated first. + if (battery_response is not None) or (server_response is not None): - if climate_response is not None: - _LOGGER.debug("Got climate data for Leaf") - _LOGGER.debug(climate_response.__dict__) - self.data[DATA_CLIMATE] = climate_response.is_hvac_running + climate_response = await self.async_get_climate() + if climate_response is not None: + _LOGGER.debug("Got climate data for Leaf.") + _LOGGER.debug(climate_response.__dict__) + self.data[DATA_CLIMATE] = climate_response.is_hvac_running if self.nissan_connect: try: - location_response = self.get_location() + location_response = await self.async_get_location() if location_response is None: _LOGGER.debug("Empty Location Response Received") @@ -242,121 +355,171 @@ def refresh_data(self): _LOGGER.debug("Location Response: ") _LOGGER.debug(location_response.__dict__) - except: + except CarwingsError: _LOGGER.error("Error fetching location info") + self.request_in_progress = False self.signal_components() def signal_components(self): - dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) - - def get_battery(self): - request = self.leaf.request_update() - battery_status = self.leaf.get_status_from_update(request) - - i = 0 - - while battery_status is None: - if i >= self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]: - _LOGGER.debug("Climate Data failed to arrive within %d attempts", - self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]) - break - - i += 1 - - _LOGGER.debug("Battery data not in yet (Attempt %d)", i) - time.sleep(5) - battery_status = self.leaf.get_status_from_update(request) - - return battery_status - - def get_climate(self): + """Signal components to refresh.""" + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) + + async def async_get_battery(self): + """Request battery update from Nissan servers.""" + # First, check nissan servers for the latest data + start_server_info = await self.hass.async_add_job( + self.leaf.get_latest_battery_status + ) + + # Store the date from the nissan servers + start_date = start_server_info.answer[ + "BatteryStatusRecords"]["OperationDateAndTime"] + _LOGGER.info("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) + + for attempt in range(MAX_RESPONSE_ATTEMPTS): + await asyncio.sleep(5) + battery_status = await self.hass.async_add_job( + self.leaf.get_status_from_update, request + ) + if battery_status is not None: + return (battery_status, None) + + _LOGGER.info("Battery data (%s) not in yet (%s). " + "Seeing if nissan server data has changed", + self.leaf.vin, attempt) + server_info = await self.hass.async_add_job( + self.leaf.get_latest_battery_status + ) + latest_date = server_info.answer[ + "BatteryStatusRecords"]["OperationDateAndTime"] + _LOGGER.info("Latest server date=%s", latest_date) + if latest_date != start_date: + _LOGGER.info("Using updated server info instead " + "of waiting for request_updated") + return (None, server_info) + + _LOGGER.info("%s attempts exceeded return latest data from server", + MAX_RESPONSE_ATTEMPTS) + return (None, server_info) + + async def async_get_climate(self): + """Request climate data from Nissan servers.""" + from pycarwings2 import CarwingsError try: - request = self.leaf.get_latest_hvac_status() + request = await self.hass.async_add_job( + self.leaf.get_latest_hvac_status + ) return request - except TypeError: + except CarwingsError: + _LOGGER.error( + "An error occurred communicating with the car %s", + self.leaf.vin) return None - def set_climate(self, toggle): + async def async_set_climate(self, toggle): + """Set climate control mode via Nissan servers.""" + climate_result = None if toggle: - request = self.leaf.start_climate_control() - climate_result = self.leaf.get_start_climate_control_result( - request) - - i = 0 + _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 5 seconds.", self.leaf.vin, attempt) + await asyncio.sleep(5) + + climate_result = await self.hass.async_add_job( + self.leaf.get_start_climate_control_result, request + ) - while climate_result is None: - if i >= self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]: - _LOGGER.debug("Climate Data failed to arrive within %d attempts", - self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]) + if climate_result is not None: break - i += 1 - - _LOGGER.debug("Climate data not in yet (Attempt %d)", i) - time.sleep(5) - climate_result = self.leaf.get_start_climate_control_result( - request) - - _LOGGER.debug(climate_result.__dict__) - - self.signal_components() - - return climate_result.is_hvac_running else: - request = self.leaf.stop_climate_control() - climate_result = self.leaf.get_stop_climate_control_result(request) - - i = 0 + _LOGGER.info("Requesting climate turn off for %s", self.leaf.vin) + request = await self.hass.async_add_job( + self.leaf.stop_climate_control + ) + + for attempt in range(MAX_RESPONSE_ATTEMPTS): + if attempt > 0: + _LOGGER.debug("Climate data not in yet. (%s) (%s). " + "Waiting 5 seconds", self.leaf.vin, attempt) + await asyncio.sleep(5) + + climate_result = await self.hass.async_add_job( + self.leaf.get_stop_climate_control_result, request + ) - while climate_result is None: - if i >= self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]: - _LOGGER.debug("Climate Data failed to arrive within %d attempts", - self.config[DOMAIN][CONF_REFRESH_ATTEMPTS]) + if climate_result is not None: break - i += 1 - - _LOGGER.debug("Climate data not in yet (Attempt %d)", i) - time.sleep(5) - climate_result = self.leaf.get_stop_climate_control_result( - request) - + if climate_result is not None: + _LOGGER.debug("Climate result:") _LOGGER.debug(climate_result.__dict__) - self.signal_components() + return climate_result.is_hvac_running == toggle - return climate_result.is_hvac_running is False - - def get_location(self): - request = self.leaf.request_location() - location_status = self.leaf.get_status_from_location(request) + _LOGGER.debug("Climate result not returned by Nissan servers") + return False - while location_status is None: - _LOGGER.debug("Location data not in yet.") - time.sleep(5) - location_status = self.leaf.get_status_from_location(request) + async def async_get_location(self): + """Get location from Nissan servers.""" + request = await self.hass.async_add_job(self.leaf.request_location) + for attempt in range(MAX_RESPONSE_ATTEMPTS): + if attempt > 0: + _LOGGER.debug("Location data not in yet. (%s) (%s). " + "Waiting 5 seconds", self.leaf.vin, attempt) + await asyncio.sleep(5) + + location_status = await self.hass.async_add_job( + self.leaf.get_status_from_location, request + ) + + if location_status is not None: + _LOGGER.debug(location_status.__dict__) + break + self.signal_components() return location_status def start_charging(self): - return self.leaf.start_charging() + """Request start charging via Nissan servers.""" + # TODO: This should be improved to: + # 1. check if the command to start charging actually completes. + # 2. reschedule the update if interval less whilst charging. + # 3. disable the start charging button + self.hass.async_add_job(self.leaf.start_charging) class LeafEntity(Entity): + """Base class for Nissan Leaf entity.""" + def __init__(self, car): + """Store LeafDataStore upon init.""" self.car = car + def log_registration(self): + """Abstract log registration.""" + raise NotImplementedError("Please implement this method") + @property def device_state_attributes(self): + """Default attributes for Nissan Leaf entities.""" return { 'homebridge_serial': self.car.leaf.vin, 'homebridge_mfg': 'Nissan', 'homebridge_model': 'Leaf' } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.log_registration() async_dispatcher_connect( diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index ab2c8934a4b5c..62cd15b36861f 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -1,5 +1,4 @@ -""" -Battery Charge and Range Support for the Nissan Leaf +"""Battery Charge and Range Support for the Nissan Leaf. Documentation pending. Please refer to the main platform component for configuration details @@ -8,7 +7,10 @@ import logging from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from homeassistant.helpers.icon import icon_for_battery_level -from .. import nissan_leaf as LeafCore +from homeassistant.components.nissan_leaf import ( + DATA_BATTERY, DATA_CHARGING, DATA_LEAF, DATA_RANGE_AC, DATA_RANGE_AC_OFF, + LeafEntity +) _LOGGER = logging.getLogger(__name__) @@ -16,72 +18,89 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - devices = [] - - _LOGGER.debug("Adding sensors") + """Sensors setup.""" + _LOGGER.debug("setup_platform nissan_leaf sensors, discovery_info=%s", + discovery_info) - for key, value in hass.data[LeafCore.DATA_LEAF].items(): + 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)) add_devices(devices, True) - return True +class LeafBatterySensor(LeafEntity): + """Nissan Leaf Battery Sensor.""" -class LeafBatterySensor(LeafCore.LeafEntity): @property def name(self): + """Sensor Name.""" return self.car.leaf.nickname + " Charge" def log_registration(self): + """Log registration.""" _LOGGER.debug( "Registered LeafBatterySensor component with HASS for VIN %s", self.car.leaf.vin) + @property + def device_class(self): + """Return the device class of the sensor.""" + return 'battery' + @property def state(self): - return round(self.car.data[LeafCore.DATA_BATTERY], 0) + """Battery state percentage.""" + return round(self.car.data[DATA_BATTERY], 0) @property def unit_of_measurement(self): + """Battery state measured in percentage.""" return '%' @property def icon(self): - chargeState = self.car.data[LeafCore.DATA_CHARGING] + """Battery state icon handling.""" + chargestate = self.car.data[DATA_CHARGING] return icon_for_battery_level( battery_level=self.state, - charging=chargeState + charging=chargestate ) -class LeafRangeSensor(LeafCore.LeafEntity): +class LeafRangeSensor(LeafEntity): + """Nissan Leaf Range Sensor.""" + def __init__(self, car, ac_on): + """Setup range sensor, indicate if AC on or off range sensor.""" self.ac_on = ac_on super().__init__(car) @property def name(self): + """Update sensor name depending on AC.""" if self.ac_on is True: return self.car.leaf.nickname + " Range (AC)" - else: - return self.car.leaf.nickname + " Range" + return self.car.leaf.nickname + " Range" def log_registration(self): + """Log registration.""" _LOGGER.debug( "Registered LeafRangeSensor component with HASS for VIN %s", self.car.leaf.vin) @property def state(self): + """Battery range in miles or kms.""" ret = 0 if self.ac_on is True: - ret = self.car.data[LeafCore.DATA_RANGE_AC] + ret = self.car.data[DATA_RANGE_AC] else: - ret = self.car.data[LeafCore.DATA_RANGE_AC_OFF] + ret = self.car.data[DATA_RANGE_AC_OFF] if (self.car.hass.config.units.is_metric is False or self.car.force_miles is True): @@ -91,12 +110,13 @@ def state(self): @property def unit_of_measurement(self): + """Battery range unit.""" if (self.car.hass.config.units.is_metric is False or self.car.force_miles is True): return "mi" - else: - return "km" + return "km" @property def icon(self): + """Nice icon for range.""" return 'mdi:speedometer' diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index 5379a00fc8c65..5b41fd0242c2c 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -1,13 +1,15 @@ """ -Charge and Climate Control Support for the Nissan Leaf +Charge and Climate Control Support for the Nissan Leaf. Documentation pending. Please refer to the main platform component for configuration details """ import logging +from homeassistant.components.nissan_leaf import ( + DATA_CHARGING, DATA_CLIMATE, DATA_LEAF, LeafEntity +) from homeassistant.helpers.entity import ToggleEntity -from .. import nissan_leaf as LeafCore _LOGGER = logging.getLogger(__name__) @@ -16,77 +18,94 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - devices = [] + """Nissan Leaf switch platform setup.""" + _LOGGER.debug("In switch setup platform, discovery_info=%s", + discovery_info) - for key, value in hass.data[LeafCore.DATA_LEAF].items(): + devices = [] + for key, value in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding switch for item key=%s, value=%s", key, value) + _LOGGER.debug("Adding LeafChargeSwitch(%s)", value) devices.append(LeafChargeSwitch(value)) + _LOGGER.debug("Adding LeafClimateSwitch(%s)", value) devices.append(LeafClimateSwitch(value)) + _LOGGER.debug("Calling add_devices for switches") add_devices(devices, True) -class LeafClimateSwitch(LeafCore.LeafEntity, ToggleEntity): +class LeafClimateSwitch(LeafEntity, ToggleEntity): + """Nissan Leaf Climate Control switch.""" + @property def name(self): + """Switch name.""" return self.car.leaf.nickname + " Climate Control" def log_registration(self): + """Log registration.""" _LOGGER.debug( "Registered LeafClimateSwitch component with HASS for VIN %s", self.car.leaf.vin) @property def is_on(self): - return self.car.data[LeafCore.DATA_CLIMATE] is True - - def turn_on(self, **kwargs): - if self.car.set_climate(True): - self.car.data[LeafCore.DATA_CLIMATE] = True + """Return true if climate control is on.""" + return self.car.data[DATA_CLIMATE] is True - self._update_callback() + async def async_turn_on(self, **kwargs): + """Turn on climate control.""" + if await self.car.async_set_climate(True): + self.car.data[DATA_CLIMATE] = True - def turn_off(self, **kwargs): - if self.car.set_climate(False): - self.car.data[LeafCore.DATA_CLIMATE] = False - - self._update_callback() + async def async_turn_off(self, **kwargs): + """Turn off climate control.""" + if await self.car.async_set_climate(False): + self.car.data[DATA_CLIMATE] = False @property def icon(self): - if self.car.data[LeafCore.DATA_CLIMATE]: + """Climate control icon.""" + if self.car.data[DATA_CLIMATE]: return 'mdi:fan' - else: - return 'mdi:fan-off' + return 'mdi:fan-off' + +class LeafChargeSwitch(LeafEntity, ToggleEntity): + """Nissan Leaf Charging On switch.""" -class LeafChargeSwitch(LeafCore.LeafEntity, ToggleEntity): @property def name(self): + """Switch name.""" return self.car.leaf.nickname + " Charging Status" def log_registration(self): + """Log registration.""" _LOGGER.debug( "Registered LeafChargeSwitch component with HASS for VIN %s", self.car.leaf.vin) @property def icon(self): - if self.car.data[LeafCore.DATA_CHARGING]: + """Charging switch icon.""" + if self.car.data[DATA_CHARGING]: return 'mdi:flash' - else: - return 'mdi:flash-off' + return 'mdi:flash-off' @property def is_on(self): - return self.car.data[LeafCore.DATA_CHARGING] is True + """Return true if charging.""" + return self.car.data[DATA_CHARGING] is True def turn_on(self, **kwargs): + """Start car charging.""" if self.car.start_charging(): - self.car.data[LeafCore.DATA_CHARGING] = True + self.car.data[DATA_CHARGING] = True self._update_callback() def turn_off(self, **kwargs): + """Nissan API doesn't allow stopping of charge remotely.""" _LOGGER.debug( "Cannot turn off Leaf charging -" " Nissan does not support that remotely.") From 77d335b6073b2c47a173e636cb0facae65a42537 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 08:12:13 +0000 Subject: [PATCH 20/78] Add pycarwings2 to requirements_all.txt --- requirements_all.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index a904d27d1bac2..67610f52bd73b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -921,6 +921,9 @@ pyblackbird==0.5 # homeassistant.components.neato pybotvac==0.0.12 +# homeassistant.components.nissan_leaf +pycarwings2==2.2 + # homeassistant.components.cloudflare pycfdns==0.0.1 From 31bfb3d22eac3468f202e182794f1e9f49f44246 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 10:26:03 +0000 Subject: [PATCH 21/78] Make stopping charge error an 'info'. Remove TODO. --- homeassistant/components/nissan_leaf.py | 4 ---- homeassistant/components/switch/nissan_leaf.py | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 2d74520fba110..450d4c224a937 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -68,9 +68,6 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -# TODO: Disable charging switch if currently charging, because charging -# cannot be stopped remotely by the Nissan APIs. - # If testing then use the following kinds of URLs # # REQUIREMENTS = ['https://github.com/filcole/pycarwings2/archive/master.zip' @@ -495,7 +492,6 @@ def start_charging(self): # TODO: This should be improved to: # 1. check if the command to start charging actually completes. # 2. reschedule the update if interval less whilst charging. - # 3. disable the start charging button self.hass.async_add_job(self.leaf.start_charging) diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index 5b41fd0242c2c..502bc18b73096 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -106,6 +106,6 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Nissan API doesn't allow stopping of charge remotely.""" - _LOGGER.debug( + _LOGGER.info( "Cannot turn off Leaf charging -" - " Nissan does not support that remotely.") + " Nissan API does not support stopping charge remotely.") From ce0f4746b7592c2a85bc57618b4d1aa93b473255 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 15:29:55 +0000 Subject: [PATCH 22/78] Request update from car after sending start charging command. --- homeassistant/components/nissan_leaf.py | 20 ++++++++++++------- .../components/switch/nissan_leaf.py | 6 ++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 450d4c224a937..51a3b5e12f809 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -228,7 +228,7 @@ def __init__(self, leaf, hass, car_config): @asyncio.coroutine async def async_update_data(self, now): """Update data from nissan leaf.""" - await self.async_refresh_data() + await self.async_refresh_data(now) next_interval = self.get_next_interval() _LOGGER.debug("Next interval=%s", next_interval) async_track_point_in_utc_time( @@ -270,7 +270,7 @@ def get_next_interval(self): return utcnow() + interval - async def async_refresh_data(self): + async def async_refresh_data(self, now): """Refresh the leaf data and update the datastore.""" from pycarwings2 import CarwingsError @@ -487,12 +487,18 @@ async def async_get_location(self): self.signal_components() return location_status - def start_charging(self): + async def async_start_charging(self): """Request start charging via Nissan servers.""" - # TODO: This should be improved to: - # 1. check if the command to start charging actually completes. - # 2. reschedule the update if interval less whilst charging. - self.hass.async_add_job(self.leaf.start_charging) + # 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) + async_track_point_in_utc_time( + self.hass, self.async_refresh_data, check_charge_at) class LeafEntity(Entity): diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index 502bc18b73096..e267d6b33061d 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -97,13 +97,11 @@ def is_on(self): """Return true if charging.""" return self.car.data[DATA_CHARGING] is True - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Start car charging.""" - if self.car.start_charging(): + if await self.car.async_start_charging(): self.car.data[DATA_CHARGING] = True - self._update_callback() - def turn_off(self, **kwargs): """Nissan API doesn't allow stopping of charge remotely.""" _LOGGER.info( From 2127dbd17359fa6d410ba15b1c6527cebbb200ff Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 15:33:02 +0000 Subject: [PATCH 23/78] Delay initial (slow) update for 15 seconds and make async --- homeassistant/components/nissan_leaf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 51a3b5e12f809..ff5381d6ed517 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -102,6 +102,7 @@ CONF_VALID_REGIONS = ['NNA', 'NE', 'NCI', 'NMA', 'NML'] CONF_FORCE_MILES = 'force_miles' +INITIAL_UPDATE = timedelta(seconds=15) MIN_UPDATE_INTERVAL = timedelta(minutes=2) DEFAULT_INTERVAL = timedelta(minutes=30) DEFAULT_CHARGING_INTERVAL = timedelta(minutes=15) @@ -188,13 +189,14 @@ async def leaf_login(): data_store = LeafDataStore(leaf, hass, car_config) hass.data[DATA_LEAF][leaf.vin] = data_store - await hass.async_add_job(data_store.async_update_data, None) for component in LEAF_COMPONENTS: if (component != 'device_tracker' or car_config[CONF_NCONNECT] is True): load_platform(hass, component, DOMAIN, {}, car_config) + async_track_point_in_utc_time(hass, data_store.async_update_data, utcnow() + INITIAL_UPDATE) + hass.data[DATA_LEAF] = {} tasks = [async_setup_leaf(car) for car in config[DOMAIN]] if tasks: From 2de677fd53fa44fc097cd1714700ca8825e6c8ba Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 15:40:15 +0000 Subject: [PATCH 24/78] Flake8 line length fixes --- homeassistant/components/nissan_leaf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index ff5381d6ed517..60f3255ebb6ff 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -195,7 +195,8 @@ async def leaf_login(): car_config[CONF_NCONNECT] is True): load_platform(hass, component, DOMAIN, {}, car_config) - async_track_point_in_utc_time(hass, data_store.async_update_data, utcnow() + INITIAL_UPDATE) + async_track_point_in_utc_time(hass, data_store.async_update_data, + utcnow() + INITIAL_UPDATE) hass.data[DATA_LEAF] = {} tasks = [async_setup_leaf(car) for car in config[DOMAIN]] @@ -497,7 +498,8 @@ async def async_start_charging(self): # 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") + _LOGGER.debug("Start charging sent, " + "request updated data in 1 minute") check_charge_at = utcnow() + timedelta(minutes=1) async_track_point_in_utc_time( self.hass, self.async_refresh_data, check_charge_at) From c0d5ba652abcb3837804aae73458f90570af5626 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 16:18:21 +0000 Subject: [PATCH 25/78] Try to fix D401 'imperative mood' git diff tox errors --- homeassistant/components/nissan_leaf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 60f3255ebb6ff..6c46a80409587 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -143,11 +143,11 @@ @asyncio.coroutine def async_setup(hass, config): - """Setup the nissan leaf component.""" + """Set-up the Nissan Leaf component.""" import pycarwings2 async def async_setup_leaf(car_config): - """Setup a leaf car.""" + """Set-up a car.""" _LOGGER.debug("Logging into You+Nissan...") username = car_config[CONF_USERNAME] @@ -518,7 +518,7 @@ def log_registration(self): @property def device_state_attributes(self): - """Default attributes for Nissan Leaf entities.""" + """Return default attributes for Nissan Leaf entities.""" return { 'homebridge_serial': self.car.leaf.vin, 'homebridge_mfg': 'Nissan', @@ -532,5 +532,5 @@ async def async_added_to_hass(self): self.car.hass, SIGNAL_UPDATE_LEAF, self._update_callback) def _update_callback(self): - """Callback update method.""" + """Update the state.""" self.schedule_update_ha_state(True) From 68a50ab206ee262670c674f1cd09da7e9f38dea6 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 16:29:18 +0000 Subject: [PATCH 26/78] Try to fix more D401 'imperative mood' tox errors --- homeassistant/components/sensor/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 62cd15b36861f..1b78d6076e428 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -75,7 +75,7 @@ class LeafRangeSensor(LeafEntity): """Nissan Leaf Range Sensor.""" def __init__(self, car, ac_on): - """Setup range sensor, indicate if AC on or off range sensor.""" + """Set-up range sensor. Store if AC on.""" self.ac_on = ac_on super().__init__(car) From bc887082d3cc4ae5604135bbb92f85e303d03816 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 21:41:11 +0000 Subject: [PATCH 27/78] Default interval of an hour in code, to match comments. --- homeassistant/components/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 6c46a80409587..b03104db09c7f 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -104,7 +104,7 @@ INITIAL_UPDATE = timedelta(seconds=15) MIN_UPDATE_INTERVAL = timedelta(minutes=2) -DEFAULT_INTERVAL = timedelta(minutes=30) +DEFAULT_INTERVAL = timedelta(hours=1) DEFAULT_CHARGING_INTERVAL = timedelta(minutes=15) DEFAULT_CLIMATE_INTERVAL = timedelta(minutes=5) RESTRICTED_BATTERY = 2 From 127c9f7465c722b8b96a537993ef64cea20534b6 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 21:49:05 +0000 Subject: [PATCH 28/78] Update to pycarwings2 2.3 --- homeassistant/components/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index b03104db09c7f..d2545b220f6b8 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -79,7 +79,7 @@ # REQUIREMENTS = ['file:///home/phil/repos/pycarwings2ve/pycarwings2/ # dist/pycarwings2-2.2.tar.gz' # '#pycarwings2'] -REQUIREMENTS = ['pycarwings2==2.2'] +REQUIREMENTS = ['pycarwings2==2.3'] _LOGGER = logging.getLogger(__name__) From f3ca2b24dadc9105529e4e52b33ca0e082236646 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 5 Jan 2019 21:52:23 +0000 Subject: [PATCH 29/78] Update to pycarwings2 2.3 in requirements_all.txt --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 67610f52bd73b..7b49bafb7d242 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ pyblackbird==0.5 pybotvac==0.0.12 # homeassistant.components.nissan_leaf -pycarwings2==2.2 +pycarwings2==2.3 # homeassistant.components.cloudflare pycfdns==0.0.1 From 1cdee2b6d28f81ed486cdea4719f5864f3b91d9d Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sun, 6 Jan 2019 11:00:21 +0000 Subject: [PATCH 30/78] Remove documentation, instead refering to home-assistant.io --- .../components/binary_sensor/nissan_leaf.py | 1 - homeassistant/components/nissan_leaf.py | 51 +------------------ .../components/sensor/nissan_leaf.py | 1 - .../components/switch/nissan_leaf.py | 1 - 4 files changed, 2 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/binary_sensor/nissan_leaf.py b/homeassistant/components/binary_sensor/nissan_leaf.py index 642c4dd5d2265..bda1e72dc05cd 100644 --- a/homeassistant/components/binary_sensor/nissan_leaf.py +++ b/homeassistant/components/binary_sensor/nissan_leaf.py @@ -1,7 +1,6 @@ """ Plugged In Status Support for the Nissan Leaf. -Documentation pending. Please refer to the main platform component for configuration details """ diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index d2545b220f6b8..3e420d4433e35 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -1,55 +1,8 @@ """ Support for the Nissan Leaf Carwings/Nissan Connect API. -Please note this is the pre-2018 API, which is still functional in the US. -The old API should continue to work for the forseeable future. - -Documentation has not been created yet, here is an example configuration block: - -nissan_leaf: - - username: "username" - password: "password" - nissan_connect: true - region: 'NE' - update_interval: - hours: 1 - update_interval_charging: - minutes: 15 - update_interval_climate: - minutes: 5 - force_miles: true - - -Notes for the above: - -region: Must be one of - 'NE' (Europe), - 'NNA' (US), - 'NCI' (Canada), - 'NMA' (Australia), - 'NML' (Japan) -nissan_connect: If your car has the updated head unit (Nissan Connect rather - than Car Wings) then you can pull the location, shown in a - device tracker. If you have a pre-2016 24kWh Leaf then you - will have CarWings and should set this to false, or it will - crash the component. -update_interval: The interval between updates if AC is off and not charging -update_interval_charging: The interval between updates if charging -update_interval_climate: The interval between updates if climate control is on - -Notes for testers: - -Please report bugs using the following logger config. - -logger: - default: critical - logs: - homeassistant.components.nissan_leaf: debug - homeassistant.components.sensor.nissan_leaf: debug - homeassistant.components.switch.nissan_leaf: debug - homeassistant.components.device_tracker.nissan_leaf: debug - - +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/nissan_leaf """ import logging diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 1b78d6076e428..5daef7434faa9 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -1,6 +1,5 @@ """Battery Charge and Range Support for the Nissan Leaf. -Documentation pending. Please refer to the main platform component for configuration details """ diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index e267d6b33061d..70e6f9efcabb0 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -1,7 +1,6 @@ """ Charge and Climate Control Support for the Nissan Leaf. -Documentation pending. Please refer to the main platform component for configuration details """ From 9417b7f4294c3f1d3a26598e0e4e65b75f38e095 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 8 Jan 2019 22:52:49 +0000 Subject: [PATCH 31/78] Remove unneeded dispatcher_send() --- homeassistant/components/device_tracker/nissan_leaf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/device_tracker/nissan_leaf.py b/homeassistant/components/device_tracker/nissan_leaf.py index 6b797ecb49071..7429460da98cb 100644 --- a/homeassistant/components/device_tracker/nissan_leaf.py +++ b/homeassistant/components/device_tracker/nissan_leaf.py @@ -41,6 +41,5 @@ def see_vehicle(): icon='mdi:car') dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) - dispatcher_send(hass, SIGNAL_UPDATE_LEAF) return True From 57141304af226ad8b9c3f59fb6d79815a2a6b075 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 8 Jan 2019 23:36:01 +0000 Subject: [PATCH 32/78] Remove unneeded requirements comments --- homeassistant/components/nissan_leaf.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 3e420d4433e35..722af6ef0f1af 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -21,17 +21,6 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -# If testing then use the following kinds of URLs -# -# REQUIREMENTS = ['https://github.com/filcole/pycarwings2/archive/master.zip' -# '#pycarwings2'] -# REQUIREMENTS = ['https://test-files.pythonhosted.org/packages/7c/ad/ -# ee27988357f1710ca9ced1a60263486415f054003ca6fa396922ca6b6bbf/ -# pycarwings2-2.2.tar.gz' -# '#pycarwings2'] -# REQUIREMENTS = ['file:///home/phil/repos/pycarwings2ve/pycarwings2/ -# dist/pycarwings2-2.2.tar.gz' -# '#pycarwings2'] REQUIREMENTS = ['pycarwings2==2.3'] _LOGGER = logging.getLogger(__name__) From 44bb5458bbc72e24f1dc675516ce310dc97a551e Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 8 Jan 2019 23:46:50 +0000 Subject: [PATCH 33/78] Combine excess debugging. --- homeassistant/components/nissan_leaf.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 722af6ef0f1af..66de036edd2e4 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -187,7 +187,7 @@ def get_next_interval(self): # The 12V battery is used when communicating with Nissan servers. # The 12V battery is charged from the traction battery when not - # connected # and when the traction battery has enough charge. To + # connected and when the traction battery has enough charge. To # avoid draining the 12V battery we shall restrict the update # frequency if low battery detected. if (self.last_battery_response is not None and @@ -232,8 +232,7 @@ async def async_refresh_data(self, now): (battery_response, server_response) = await self.async_get_battery() if battery_response is not None: - _LOGGER.debug("Battery Response: ") - _LOGGER.debug(battery_response.__dict__) + _LOGGER.debug("Battery Response: %s", battery_response.__dict__) if battery_response.answer['status'] == 200: if int(battery_response.battery_capacity) > 100: @@ -255,8 +254,7 @@ async def async_refresh_data(self, now): self.last_battery_response = utcnow() if server_response is not None: - _LOGGER.debug("Server Response: ") - _LOGGER.debug(server_response.__dict__) + _LOGGER.debug("Server Response: %s", server_response.__dict__) if server_response.answer['status'] == 200: if server_response.state_of_charge is not None: @@ -280,8 +278,8 @@ async def async_refresh_data(self, now): climate_response = await self.async_get_climate() if climate_response is not None: - _LOGGER.debug("Got climate data for Leaf.") - _LOGGER.debug(climate_response.__dict__) + _LOGGER.debug("Got climate data for Leaf: %s", + climate_response.__dict__) self.data[DATA_CLIMATE] = climate_response.is_hvac_running if self.nissan_connect: @@ -295,8 +293,8 @@ async def async_refresh_data(self, now): _LOGGER.debug("Got location data for Leaf") self.data[DATA_LOCATION] = location_response - _LOGGER.debug("Location Response: ") - _LOGGER.debug(location_response.__dict__) + _LOGGER.debug("Location Response: %s", + location_response.__dict__) except CarwingsError: _LOGGER.error("Error fetching location info") @@ -404,8 +402,7 @@ async def async_set_climate(self, toggle): break if climate_result is not None: - _LOGGER.debug("Climate result:") - _LOGGER.debug(climate_result.__dict__) + _LOGGER.debug("Climate result: %s", climate_result.__dict__) self.signal_components() return climate_result.is_hvac_running == toggle @@ -426,7 +423,7 @@ async def async_get_location(self): ) if location_status is not None: - _LOGGER.debug(location_status.__dict__) + _LOGGER.debug("Location_status=%s", location_status.__dict__) break self.signal_components() From 33fcd3f60e2cda2ba7b25aeb8d0b39c892ea008a Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 8 Jan 2019 23:55:38 +0000 Subject: [PATCH 34/78] Remove single line method signal_components() --- homeassistant/components/nissan_leaf.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 66de036edd2e4..b89e87e4af6c0 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -250,7 +250,7 @@ async def async_refresh_data(self, now): self.data[DATA_RANGE_AC_OFF] = ( battery_response.cruising_range_ac_off_km ) - self.signal_components() + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) self.last_battery_response = utcnow() if server_response is not None: @@ -270,7 +270,7 @@ async def async_refresh_data(self, now): self.data[DATA_RANGE_AC_OFF] = ( server_response.cruising_range_ac_off_km ) - self.signal_components() + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) self.last_battery_response = utcnow() # Climate response only updated if battery data updated first. @@ -299,10 +299,6 @@ async def async_refresh_data(self, now): _LOGGER.error("Error fetching location info") self.request_in_progress = False - self.signal_components() - - def signal_components(self): - """Signal components to refresh.""" async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) async def async_get_battery(self): @@ -403,7 +399,7 @@ async def async_set_climate(self, toggle): if climate_result is not None: _LOGGER.debug("Climate result: %s", climate_result.__dict__) - self.signal_components() + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) return climate_result.is_hvac_running == toggle _LOGGER.debug("Climate result not returned by Nissan servers") @@ -426,7 +422,7 @@ async def async_get_location(self): _LOGGER.debug("Location_status=%s", location_status.__dict__) break - self.signal_components() + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) return location_status async def async_start_charging(self): From 587f9b5a19bf8dfcda1100f0f79f0247ca1193f3 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sun, 13 Jan 2019 15:57:52 +0000 Subject: [PATCH 35/78] Bump to version 2.4 of pycarwings2 --- homeassistant/components/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index b89e87e4af6c0..993a4af62808d 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pycarwings2==2.3'] +REQUIREMENTS = ['pycarwings2==2.4'] _LOGGER = logging.getLogger(__name__) From e4f61650f609e83c08f8d52510d7d36b4cd33e72 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sun, 13 Jan 2019 16:06:23 +0000 Subject: [PATCH 36/78] Remove unused dispatcher_send --- homeassistant/components/device_tracker/nissan_leaf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/nissan_leaf.py b/homeassistant/components/device_tracker/nissan_leaf.py index 7429460da98cb..46213075c170a 100644 --- a/homeassistant/components/device_tracker/nissan_leaf.py +++ b/homeassistant/components/device_tracker/nissan_leaf.py @@ -6,8 +6,7 @@ """ import logging from homeassistant.util import slugify -from homeassistant.helpers.dispatcher import ( - dispatcher_connect, dispatcher_send) +from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.components.nissan_leaf import ( DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF ) From 73d77594c5e94af60e04cd368f71d867275e44e2 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sun, 13 Jan 2019 16:23:14 +0000 Subject: [PATCH 37/78] Simplify logging of LeafEntity registration --- homeassistant/components/binary_sensor/nissan_leaf.py | 6 ------ homeassistant/components/nissan_leaf.py | 6 ++++-- homeassistant/components/sensor/nissan_leaf.py | 6 ------ homeassistant/components/switch/nissan_leaf.py | 6 ------ 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/binary_sensor/nissan_leaf.py b/homeassistant/components/binary_sensor/nissan_leaf.py index bda1e72dc05cd..c8ffab8a48212 100644 --- a/homeassistant/components/binary_sensor/nissan_leaf.py +++ b/homeassistant/components/binary_sensor/nissan_leaf.py @@ -36,12 +36,6 @@ def name(self): """Sensor name.""" return self.car.leaf.nickname + " Plug Status" - def log_registration(self): - """Log registration.""" - _LOGGER.debug( - "Registered LeafPluggedInSensor component with HASS for VIN %s", - self.car.leaf.vin) - @property def state(self): """Return true if plugged in.""" diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 993a4af62808d..54e09279324d9 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -448,8 +448,10 @@ def __init__(self, car): self.car = car def log_registration(self): - """Abstract log registration.""" - raise NotImplementedError("Please implement this method") + """Log registration.""" + _LOGGER.debug( + "Registered %s component for VIN %s", + self.__class__.__name__, self.car.leaf.vin) @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 5daef7434faa9..af40f2d11856c 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -39,12 +39,6 @@ def name(self): """Sensor Name.""" return self.car.leaf.nickname + " Charge" - def log_registration(self): - """Log registration.""" - _LOGGER.debug( - "Registered LeafBatterySensor component with HASS for VIN %s", - self.car.leaf.vin) - @property def device_class(self): """Return the device class of the sensor.""" diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index 70e6f9efcabb0..ed9ad95195c01 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -78,12 +78,6 @@ def name(self): """Switch name.""" return self.car.leaf.nickname + " Charging Status" - def log_registration(self): - """Log registration.""" - _LOGGER.debug( - "Registered LeafChargeSwitch component with HASS for VIN %s", - self.car.leaf.vin) - @property def icon(self): """Charging switch icon.""" From 3958ca5485afc954423ac97c1f06822da89b7ca9 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sun, 13 Jan 2019 20:33:31 +0000 Subject: [PATCH 38/78] Update requirements_all.txt --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 7b49bafb7d242..ffcd7789b3b2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ pyblackbird==0.5 pybotvac==0.0.12 # homeassistant.components.nissan_leaf -pycarwings2==2.3 +pycarwings2==2.4 # homeassistant.components.cloudflare pycfdns==0.0.1 From 46436564e11912496daac67a95f306c5433e72bb Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 21 Jan 2019 23:00:08 +0000 Subject: [PATCH 39/78] Multiple changes Increase timeout to 30 seconds Only consider battery_status Fix plugged in status Better attempts at try/exception handling --- homeassistant/components/nissan_leaf.py | 144 ++++++++++++------------ 1 file changed, 69 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 54e09279324d9..22a90b45a0b7d 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -54,6 +54,8 @@ MAX_RESPONSE_ATTEMPTS = 22 +PYCARWINGS2_SLEEP = 30 + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ vol.Required(CONF_USERNAME): cv.string, @@ -229,58 +231,35 @@ async def async_refresh_data(self, now): self.last_check = datetime.today() self.request_in_progress = True - (battery_response, server_response) = await self.async_get_battery() - - if battery_response is not None: - _LOGGER.debug("Battery Response: %s", battery_response.__dict__) - - if battery_response.answer['status'] == 200: - if int(battery_response.battery_capacity) > 100: - self.data[DATA_BATTERY] = ( - battery_response.battery_percent * 0.05 - ) - else: - self.data[DATA_BATTERY] = battery_response.battery_percent - - self.data[DATA_CHARGING] = battery_response.is_charging - self.data[DATA_PLUGGED_IN] = battery_response.is_connected - self.data[DATA_RANGE_AC] = ( - battery_response.cruising_range_ac_on_km - ) - self.data[DATA_RANGE_AC_OFF] = ( - battery_response.cruising_range_ac_off_km - ) - async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) - self.last_battery_response = utcnow() + server_response = await self.async_get_battery() if server_response is not None: _LOGGER.debug("Server Response: %s", server_response.__dict__) if server_response.answer['status'] == 200: - if server_response.state_of_charge is not None: - self.data[DATA_BATTERY] = int( - server_response.state_of_charge - ) - else: - self.data[DATA_BATTERY] = server_response.battery_percent - + self.data[DATA_BATTERY] = server_response.battery_percent self.data[DATA_RANGE_AC] = ( server_response.cruising_range_ac_on_km ) self.data[DATA_RANGE_AC_OFF] = ( server_response.cruising_range_ac_off_km ) + self.data[DATA_PLUGGED_IN] = ( + server_response.is_connected + ) async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) self.last_battery_response = utcnow() # Climate response only updated if battery data updated first. - if (battery_response is not None) or (server_response is not None): - - climate_response = await self.async_get_climate() - if climate_response is not None: - _LOGGER.debug("Got climate data for Leaf: %s", - climate_response.__dict__) - self.data[DATA_CLIMATE] = climate_response.is_hvac_running + if server_response is not None: + try: + climate_response = await self.async_get_climate() + if climate_response is not None: + _LOGGER.debug("Got climate data for Leaf: %s", + climate_response.__dict__) + self.data[DATA_CLIMATE] = climate_response.is_hvac_running + except CarwingsError: + _LOGGER.error("Error fetching climate info") if self.nissan_connect: try: @@ -301,47 +280,61 @@ async def async_refresh_data(self, now): self.request_in_progress = False async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) + @staticmethod + def _extract_start_date(battery_info): + """Extract the server date from the battery response.""" + try: + return battery_info.answer[ + "BatteryStatusRecords"]["OperationDateAndTime"] + except KeyError: + return None + async def async_get_battery(self): """Request battery update from Nissan servers.""" - # First, check nissan servers for the latest data - start_server_info = await self.hass.async_add_job( - self.leaf.get_latest_battery_status - ) - - # Store the date from the nissan servers - start_date = start_server_info.answer[ - "BatteryStatusRecords"]["OperationDateAndTime"] - _LOGGER.info("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) - - for attempt in range(MAX_RESPONSE_ATTEMPTS): - await asyncio.sleep(5) - battery_status = await self.hass.async_add_job( - self.leaf.get_status_from_update, request - ) - if battery_status is not None: - return (battery_status, None) - - _LOGGER.info("Battery data (%s) not in yet (%s). " - "Seeing if nissan server data has changed", - self.leaf.vin, attempt) - server_info = await self.hass.async_add_job( + 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 ) - latest_date = server_info.answer[ - "BatteryStatusRecords"]["OperationDateAndTime"] - _LOGGER.info("Latest server date=%s", latest_date) - if latest_date != start_date: - _LOGGER.info("Using updated server info instead " - "of waiting for request_updated") - return (None, server_info) - _LOGGER.info("%s attempts exceeded return latest data from server", - MAX_RESPONSE_ATTEMPTS) - return (None, server_info) + # Store the date from the nissan servers + start_date = self._extract_start_date(start_server_info) + if start_date is None: + _LOGGER.info("No start date from servers. Aborting") + return None + + _LOGGER.info("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) + if not request: + _LOGGER.error("Battery update request failed") + return None + + for _ in range(MAX_RESPONSE_ATTEMPTS): + _LOGGER.info("Waiting %s seconds for battery update", + PYCARWINGS2_SLEEP) + 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 + ) + + latest_date = self._extract_start_date(server_info) + _LOGGER.info("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) + return server_info + except CarwingsError: + _LOGGER.error("An error occurred getting battery status.") + return None async def async_get_climate(self): """Request climate data from Nissan servers.""" @@ -368,8 +361,9 @@ async def async_set_climate(self, toggle): for attempt in range(MAX_RESPONSE_ATTEMPTS): if attempt > 0: _LOGGER.info("Climate data not in yet (%s) (%s). " - "Waiting 5 seconds.", self.leaf.vin, attempt) - await asyncio.sleep(5) + "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 From ad2813823a33ba62541ee52028b6764c4f845b6f Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 22 Jan 2019 06:40:05 +0000 Subject: [PATCH 40/78] Fix line length --- homeassistant/components/nissan_leaf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 22a90b45a0b7d..eb6e2747a1eaa 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -361,8 +361,8 @@ async def async_set_climate(self, toggle): 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) + "Waiting (%s) seconds.", self.leaf.vin, + attempt, PYCARWINGS2_SLEEP) await asyncio.sleep(PYCARWINGS2_SLEEP) climate_result = await self.hass.async_add_job( From 4b05d8c264652d665421ad0da038ba92b9223dda Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 22 Jan 2019 06:49:50 +0000 Subject: [PATCH 41/78] Use pycarwings 2.5 --- homeassistant/components/nissan_leaf.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index eb6e2747a1eaa..70622dcd12ae6 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pycarwings2==2.4'] +REQUIREMENTS = ['pycarwings2==2.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ffcd7789b3b2e..e4532ac88ef86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ pyblackbird==0.5 pybotvac==0.0.12 # homeassistant.components.nissan_leaf -pycarwings2==2.4 +pycarwings2==2.5 # homeassistant.components.cloudflare pycfdns==0.0.1 From 192496caf07454160c87f3e86c5afac2cd017f58 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 22 Jan 2019 06:59:31 +0000 Subject: [PATCH 42/78] Remove pointless 'is True' --- homeassistant/components/sensor/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index af40f2d11856c..5a064c737d3d6 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -90,7 +90,7 @@ def state(self): """Battery range in miles or kms.""" ret = 0 - if self.ac_on is True: + if self.ac_on: ret = self.car.data[DATA_RANGE_AC] else: ret = self.car.data[DATA_RANGE_AC_OFF] From 69d4efcf4f5a6451fb8f44bf5b35394f584d6531 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 22 Jan 2019 07:05:58 +0000 Subject: [PATCH 43/78] Remove unnecessary 'is True/False' --- homeassistant/components/sensor/nissan_leaf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 5a064c737d3d6..5ac4b3eaa0404 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -95,8 +95,8 @@ def state(self): else: ret = self.car.data[DATA_RANGE_AC_OFF] - if (self.car.hass.config.units.is_metric is False or - self.car.force_miles is True): + if (not self.car.hass.config.units.is_metric or + self.car.force_miles): ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) return round(ret, 0) From 38feddb52b7b81dcd8458fbfff82a24867b77947 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 22 Jan 2019 07:07:18 +0000 Subject: [PATCH 44/78] Remove unnecessary 'is True/False' --- homeassistant/components/sensor/nissan_leaf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 5ac4b3eaa0404..26e0c3cd421bf 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -104,8 +104,8 @@ def state(self): @property def unit_of_measurement(self): """Battery range unit.""" - if (self.car.hass.config.units.is_metric is False or - self.car.force_miles is True): + if (not self.car.hass.config.units.is_metric or + self.car.force_miles): return "mi" return "km" From ef29dd2a7f8e90ebfcfb7f1f5b11b964e2e2a63b Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 22 Jan 2019 07:15:15 +0000 Subject: [PATCH 45/78] Use LENGTH_MILES and LENGTH_KILOMETERS --- homeassistant/components/sensor/nissan_leaf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 26e0c3cd421bf..602225a0fc5de 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -5,6 +5,7 @@ import logging from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.components.nissan_leaf import ( DATA_BATTERY, DATA_CHARGING, DATA_LEAF, DATA_RANGE_AC, DATA_RANGE_AC_OFF, @@ -106,8 +107,8 @@ def unit_of_measurement(self): """Battery range unit.""" if (not self.car.hass.config.units.is_metric or self.car.force_miles): - return "mi" - return "km" + return LENGTH_MILES + return LENGTH_KILOMETERS @property def icon(self): From d2379a567436ac6d88b5f25d77e282fbba4e75ea Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 22 Jan 2019 07:17:25 +0000 Subject: [PATCH 46/78] Remove excess logging in setup_platform() --- homeassistant/components/switch/nissan_leaf.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index ed9ad95195c01..179cf56321721 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -23,13 +23,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for key, value in hass.data[DATA_LEAF].items(): - _LOGGER.debug("Adding switch for item key=%s, value=%s", key, value) - _LOGGER.debug("Adding LeafChargeSwitch(%s)", value) devices.append(LeafChargeSwitch(value)) - _LOGGER.debug("Adding LeafClimateSwitch(%s)", value) devices.append(LeafClimateSwitch(value)) - _LOGGER.debug("Calling add_devices for switches") add_devices(devices, True) From 67508b2202257dcd907ff90992e80c2c1e109b86 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 22 Jan 2019 07:18:20 +0000 Subject: [PATCH 47/78] Remove unnecessary 'is True' --- homeassistant/components/switch/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index 179cf56321721..b708f9172864b 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -46,7 +46,7 @@ def log_registration(self): @property def is_on(self): """Return true if climate control is on.""" - return self.car.data[DATA_CLIMATE] is True + return self.car.data[DATA_CLIMATE] async def async_turn_on(self, **kwargs): """Turn on climate control.""" From e52ca41c24fe7b6b5f6501c73d710f868b188f83 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Tue, 22 Jan 2019 21:07:01 +0000 Subject: [PATCH 48/78] Use pycarwings2 version 2.6 --- homeassistant/components/nissan_leaf.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 70622dcd12ae6..d71d2083db041 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pycarwings2==2.5'] +REQUIREMENTS = ['pycarwings2==2.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e4532ac88ef86..ca972373c677c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ pyblackbird==0.5 pybotvac==0.0.12 # homeassistant.components.nissan_leaf -pycarwings2==2.5 +pycarwings2==2.6 # homeassistant.components.cloudflare pycfdns==0.0.1 From a262f970db846a53b2d26a9bba455dc5cc558d4e Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 25 Jan 2019 06:01:26 +0000 Subject: [PATCH 49/78] Require pycarwings2 version 2.7. --- homeassistant/components/nissan_leaf.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index d71d2083db041..084a2c2b7c541 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pycarwings2==2.6'] +REQUIREMENTS = ['pycarwings2==2.7'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ca972373c677c..f6f176459d0a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ pyblackbird==0.5 pybotvac==0.0.12 # homeassistant.components.nissan_leaf -pycarwings2==2.6 +pycarwings2==2.7 # homeassistant.components.cloudflare pycfdns==0.0.1 From 466b7754cfcd3ae83d1bb8b41d8dabce095d9d6f Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 25 Jan 2019 08:11:11 +0000 Subject: [PATCH 50/78] Increase sleep delay for climate and location reponses. --- homeassistant/components/nissan_leaf.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 084a2c2b7c541..b44a21e81f57e 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -381,8 +381,9 @@ async def async_set_climate(self, toggle): for attempt in range(MAX_RESPONSE_ATTEMPTS): if attempt > 0: _LOGGER.debug("Climate data not in yet. (%s) (%s). " - "Waiting 5 seconds", self.leaf.vin, attempt) - await asyncio.sleep(5) + "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 @@ -405,8 +406,9 @@ async def async_get_location(self): for attempt in range(MAX_RESPONSE_ATTEMPTS): if attempt > 0: _LOGGER.debug("Location data not in yet. (%s) (%s). " - "Waiting 5 seconds", self.leaf.vin, attempt) - await asyncio.sleep(5) + "Waiting %s seconds", self.leaf.vin, + attempt, PYCARWINGS2_SLEEP) + await asyncio.sleep(PYCARWINGS2_SLEEP) location_status = await self.hass.async_add_job( self.leaf.get_status_from_location, request From 5dee035568ce098fd578ef4e6c07e7dc5e946540 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Wed, 30 Jan 2019 19:02:03 +0000 Subject: [PATCH 51/78] Remove unnecessary 'is True' --- homeassistant/components/switch/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index b708f9172864b..e0f134b9a6f00 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -84,7 +84,7 @@ def icon(self): @property def is_on(self): """Return true if charging.""" - return self.car.data[DATA_CHARGING] is True + return self.car.data[DATA_CHARGING] async def async_turn_on(self, **kwargs): """Start car charging.""" From 72924b812409f026ae6a7ec694f44c0cd9e1c14a Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Wed, 30 Jan 2019 19:08:13 +0000 Subject: [PATCH 52/78] Increase frequent polling warning to _LOGGER.warning() --- homeassistant/components/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index b44a21e81f57e..1dc9af6ebb8a8 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -125,7 +125,7 @@ async def leaf_login(): return False _LOGGER.info("Successfully logged in and fetched Leaf info") - _LOGGER.info( + _LOGGER.warning( "WARNING: This may poll your Leaf too often, and drain the 12V" " battery. If you drain your cars 12V battery it WILL NOT START" " as the drive train battery won't connect." From 39b18d9040777b292dd5485759b8b7e701b6d0f7 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Wed, 30 Jan 2019 19:15:40 +0000 Subject: [PATCH 53/78] Use DEVICE_CLASS_BATTERY --- homeassistant/components/sensor/nissan_leaf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 602225a0fc5de..94d3209fad718 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -4,6 +4,7 @@ """ import logging +from homeassistant.const import DEVICE_CLASS_BATTERY from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES from homeassistant.helpers.icon import icon_for_battery_level @@ -43,7 +44,7 @@ def name(self): @property def device_class(self): """Return the device class of the sensor.""" - return 'battery' + return DEVICE_CLASS_BATTERY @property def state(self): From 50034e67a4d8ed324b638cb0bcf94de3f2da7829 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Wed, 30 Jan 2019 19:59:40 +0000 Subject: [PATCH 54/78] Remove extraneous 'is True'. --- homeassistant/components/nissan_leaf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 1dc9af6ebb8a8..4398cff0711f2 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -135,8 +135,7 @@ async def leaf_login(): hass.data[DATA_LEAF][leaf.vin] = data_store for component in LEAF_COMPONENTS: - if (component != 'device_tracker' or - car_config[CONF_NCONNECT] is True): + if component != 'device_tracker' or car_config[CONF_NCONNECT]: load_platform(hass, component, DOMAIN, {}, car_config) async_track_point_in_utc_time(hass, data_store.async_update_data, From c09752f9a6e57321c0201f1227c0fe224d2d84cf Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 1 Feb 2019 16:50:59 +0000 Subject: [PATCH 55/78] Move icon strings to constants. --- homeassistant/components/device_tracker/nissan_leaf.py | 4 +++- homeassistant/components/sensor/nissan_leaf.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/nissan_leaf.py b/homeassistant/components/device_tracker/nissan_leaf.py index 46213075c170a..6394ca19881fb 100644 --- a/homeassistant/components/device_tracker/nissan_leaf.py +++ b/homeassistant/components/device_tracker/nissan_leaf.py @@ -15,6 +15,8 @@ DEPENDENCIES = ['nissan_leaf'] +ICON_CAR = "mdi:car" + def setup_scanner(hass, config, see, discovery_info=None): """Set up the Nissan Leaf tracker.""" @@ -37,7 +39,7 @@ def see_vehicle(): value.data[DATA_LOCATION].latitude, value.data[DATA_LOCATION].longitude ), - icon='mdi:car') + icon=ICON_CAR) dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 94d3209fad718..220b4967cf29a 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -17,6 +17,7 @@ DEPENDENCIES = ['nissan_leaf'] +ICON_RANGE = 'mdi:speedometer' def setup_platform(hass, config, add_devices, discovery_info=None): """Sensors setup.""" @@ -114,4 +115,4 @@ def unit_of_measurement(self): @property def icon(self): """Nice icon for range.""" - return 'mdi:speedometer' + return ICON_RANGE From dc212c4bd224a9795d95d09445f97c3bb5c4b4d2 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 1 Feb 2019 16:56:51 +0000 Subject: [PATCH 56/78] Remove unneeded key. --- homeassistant/components/switch/nissan_leaf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/switch/nissan_leaf.py index e0f134b9a6f00..20eced258e6f0 100644 --- a/homeassistant/components/switch/nissan_leaf.py +++ b/homeassistant/components/switch/nissan_leaf.py @@ -22,7 +22,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): discovery_info) devices = [] - for key, value in hass.data[DATA_LEAF].items(): + for value in hass.data[DATA_LEAF].values(): devices.append(LeafChargeSwitch(value)) devices.append(LeafClimateSwitch(value)) From 3c7c509506dfa4e39afed8eecfbd468569fba004 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 1 Feb 2019 17:08:34 +0000 Subject: [PATCH 57/78] LeafRangeSensor ac_on property is internal. --- homeassistant/components/sensor/nissan_leaf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 220b4967cf29a..9edfa603bfe31 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -72,13 +72,13 @@ class LeafRangeSensor(LeafEntity): def __init__(self, car, ac_on): """Set-up range sensor. Store if AC on.""" - self.ac_on = ac_on + self._ac_on = ac_on super().__init__(car) @property def name(self): """Update sensor name depending on AC.""" - if self.ac_on is True: + if self._ac_on is True: return self.car.leaf.nickname + " Range (AC)" return self.car.leaf.nickname + " Range" @@ -93,7 +93,7 @@ def state(self): """Battery range in miles or kms.""" ret = 0 - if self.ac_on: + if self._ac_on: ret = self.car.data[DATA_RANGE_AC] else: ret = self.car.data[DATA_RANGE_AC_OFF] From 9ce043011371622adaca21cdee65a6c9a37063c3 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 1 Feb 2019 17:15:13 +0000 Subject: [PATCH 58/78] Flake8 missing line --- homeassistant/components/sensor/nissan_leaf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 9edfa603bfe31..1f29544084070 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -19,6 +19,7 @@ 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", From cf0b761be652865a4bc96c53000addd29d53b0a2 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 1 Feb 2019 17:37:30 +0000 Subject: [PATCH 59/78] Remove homebridge attributes. --- homeassistant/components/nissan_leaf.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 4398cff0711f2..1340e6ce29414 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -448,15 +448,6 @@ def log_registration(self): "Registered %s component for VIN %s", self.__class__.__name__, self.car.leaf.vin) - @property - def device_state_attributes(self): - """Return default attributes for Nissan Leaf entities.""" - return { - 'homebridge_serial': self.car.leaf.vin, - 'homebridge_mfg': 'Nissan', - 'homebridge_model': 'Leaf' - } - async def async_added_to_hass(self): """Register callbacks.""" self.log_registration() From bc6d50fb3d22f31ddfe2afee903902cbbac5ed3b Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 4 Feb 2019 19:13:57 +0000 Subject: [PATCH 60/78] Remove round battery % and range to whole numbers --- homeassistant/components/sensor/nissan_leaf.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/sensor/nissan_leaf.py index 1f29544084070..71474f8a422bb 100644 --- a/homeassistant/components/sensor/nissan_leaf.py +++ b/homeassistant/components/sensor/nissan_leaf.py @@ -51,7 +51,7 @@ def device_class(self): @property def state(self): """Battery state percentage.""" - return round(self.car.data[DATA_BATTERY], 0) + return round(self.car.data[DATA_BATTERY]) @property def unit_of_measurement(self): @@ -92,8 +92,6 @@ def log_registration(self): @property def state(self): """Battery range in miles or kms.""" - ret = 0 - if self._ac_on: ret = self.car.data[DATA_RANGE_AC] else: @@ -103,7 +101,7 @@ def state(self): self.car.force_miles): ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) - return round(ret, 0) + return round(ret) @property def unit_of_measurement(self): From 81c87d528ca7fe9d4103818a8b29b25e4020d9b1 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 4 Feb 2019 19:34:00 +0000 Subject: [PATCH 61/78] Use pycarwings2 2.8 --- homeassistant/components/nissan_leaf.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf.py index 1340e6ce29414..4f31d22fef184 100644 --- a/homeassistant/components/nissan_leaf.py +++ b/homeassistant/components/nissan_leaf.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pycarwings2==2.7'] +REQUIREMENTS = ['pycarwings2==2.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 737d021c32c6a..d414a33a69605 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -937,7 +937,7 @@ pyblackbird==0.5 pybotvac==0.0.13 # homeassistant.components.nissan_leaf -pycarwings2==2.7 +pycarwings2==2.8 # homeassistant.components.cloudflare pycfdns==0.0.1 From 676c9ef2c483d4ad3d18c42a6e6220d7b2ff6325 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 4 Feb 2019 22:27:57 +0000 Subject: [PATCH 62/78] Move to embedded component model --- .../components/{nissan_leaf.py => nissan_leaf/__init__.py} | 0 .../nissan_leaf.py => nissan_leaf/binary_sensor.py} | 0 .../nissan_leaf.py => nissan_leaf/device_tracker.py} | 0 .../components/{sensor/nissan_leaf.py => nissan_leaf/sensor.py} | 0 .../components/{switch/nissan_leaf.py => nissan_leaf/switch.py} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename homeassistant/components/{nissan_leaf.py => nissan_leaf/__init__.py} (100%) rename homeassistant/components/{binary_sensor/nissan_leaf.py => nissan_leaf/binary_sensor.py} (100%) rename homeassistant/components/{device_tracker/nissan_leaf.py => nissan_leaf/device_tracker.py} (100%) rename homeassistant/components/{sensor/nissan_leaf.py => nissan_leaf/sensor.py} (100%) rename homeassistant/components/{switch/nissan_leaf.py => nissan_leaf/switch.py} (100%) diff --git a/homeassistant/components/nissan_leaf.py b/homeassistant/components/nissan_leaf/__init__.py similarity index 100% rename from homeassistant/components/nissan_leaf.py rename to homeassistant/components/nissan_leaf/__init__.py diff --git a/homeassistant/components/binary_sensor/nissan_leaf.py b/homeassistant/components/nissan_leaf/binary_sensor.py similarity index 100% rename from homeassistant/components/binary_sensor/nissan_leaf.py rename to homeassistant/components/nissan_leaf/binary_sensor.py diff --git a/homeassistant/components/device_tracker/nissan_leaf.py b/homeassistant/components/nissan_leaf/device_tracker.py similarity index 100% rename from homeassistant/components/device_tracker/nissan_leaf.py rename to homeassistant/components/nissan_leaf/device_tracker.py diff --git a/homeassistant/components/sensor/nissan_leaf.py b/homeassistant/components/nissan_leaf/sensor.py similarity index 100% rename from homeassistant/components/sensor/nissan_leaf.py rename to homeassistant/components/nissan_leaf/sensor.py diff --git a/homeassistant/components/switch/nissan_leaf.py b/homeassistant/components/nissan_leaf/switch.py similarity index 100% rename from homeassistant/components/switch/nissan_leaf.py rename to homeassistant/components/nissan_leaf/switch.py From b25a4f49a0f813f2ab37d246ee146fe8393a0bbc Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 4 Feb 2019 22:28:49 +0000 Subject: [PATCH 63/78] Reduce maximum attempts to 10 (5 mins) --- homeassistant/components/nissan_leaf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 4f31d22fef184..4baa9873c681f 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -52,7 +52,7 @@ RESTRICTED_BATTERY = 2 RESTRICTED_INTERVAL = timedelta(hours=12) -MAX_RESPONSE_ATTEMPTS = 22 +MAX_RESPONSE_ATTEMPTS = 10 PYCARWINGS2_SLEEP = 30 From ddd5815fcd055150c5c124c28a79d3d6c5668370 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 4 Feb 2019 22:48:38 +0000 Subject: [PATCH 64/78] Include attempt count in 'waiting' log message --- homeassistant/components/nissan_leaf/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 4baa9873c681f..1cd35e509d292 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -312,9 +312,9 @@ async def async_get_battery(self): _LOGGER.error("Battery update request failed") return None - for _ in range(MAX_RESPONSE_ATTEMPTS): - _LOGGER.info("Waiting %s seconds for battery update", - PYCARWINGS2_SLEEP) + for attempt in range(MAX_RESPONSE_ATTEMPTS): + _LOGGER.info("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 From 60b91781f18dc7c9c889a49902af8fb60aaba196 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 4 Feb 2019 23:00:30 +0000 Subject: [PATCH 65/78] Use await instead of yield. Remove @asyncio.coroutine decorators. --- homeassistant/components/nissan_leaf/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 1cd35e509d292..0c249b691b6df 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -85,8 +85,7 @@ SIGNAL_UPDATE_LEAF = 'nissan_leaf_update' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set-up the Nissan Leaf component.""" import pycarwings2 @@ -144,7 +143,7 @@ async def leaf_login(): hass.data[DATA_LEAF] = {} tasks = [async_setup_leaf(car) for car in config[DOMAIN]] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) return True @@ -171,7 +170,6 @@ def __init__(self, leaf, hass, car_config): self.last_battery_response = None self.request_in_progress = False - @asyncio.coroutine async def async_update_data(self, now): """Update data from nissan leaf.""" await self.async_refresh_data(now) From f75b8d79fa5740304ee9daac1fb20d732864a8d5 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 4 Feb 2019 23:03:10 +0000 Subject: [PATCH 66/78] Add @filcole as nissan_leaf codeowner --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index a0d67c6191df8..1b8279ec3e398 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -220,6 +220,7 @@ homeassistant/components/*/mystrom.py @fabaff # N homeassistant/components/ness_alarm.py @nickw444 homeassistant/components/*/ness_alarm.py @nickw444 +homeassistant/components/nissan_leaf/* @filcole # O homeassistant/components/openuv/* @bachya From c19a36298f4dd31403af85c0d7fdd6cb4ad7682a Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 9 Feb 2019 12:24:45 +0000 Subject: [PATCH 67/78] Fix checking for if not data returned from vehicle. Don't double send signal on location update. --- homeassistant/components/nissan_leaf/__init__.py | 1 - homeassistant/components/nissan_leaf/device_tracker.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 0c249b691b6df..ece28473ed5a3 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -415,7 +415,6 @@ async def async_get_location(self): _LOGGER.debug("Location_status=%s", location_status.__dict__) break - async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) return location_status async def async_start_charging(self): diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py index 6394ca19881fb..40195d9a9576d 100644 --- a/homeassistant/components/nissan_leaf/device_tracker.py +++ b/homeassistant/components/nissan_leaf/device_tracker.py @@ -28,11 +28,12 @@ def see_vehicle(): for key, value in hass.data[DATA_LEAF].items(): host_name = value.leaf.nickname dev_id = 'nissan_leaf_{}'.format(slugify(host_name)) - if value.data[DATA_LOCATION] in [None, False]: + if not value.data[DATA_LOCATION]: _LOGGER.debug("No position found for vehicle %s", key) return False _LOGGER.debug("Updating device_tracker for %s with position %s", - value.leaf.nickname, value.data[DATA_LOCATION]) + value.leaf.nickname, + value.data[DATA_LOCATION].__dict__) see(dev_id=dev_id, host_name=host_name, gps=( From 30c795df6ce315525e7c8c31c0a9c60de5244c4e Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 9 Feb 2019 15:09:11 +0000 Subject: [PATCH 68/78] Exposed updated_on, update_in_progress and next_update attributes. --- .../components/nissan_leaf/__init__.py | 24 +++++++++++++++++-- .../components/nissan_leaf/switch.py | 8 +++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index ece28473ed5a3..0d5522beaa7ae 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -166,9 +166,14 @@ def __init__(self, leaf, hass, car_config): self.data[DATA_RANGE_AC] = 0 self.data[DATA_RANGE_AC_OFF] = 0 self.data[DATA_PLUGGED_IN] = False + self.next_update = None self.last_check = None - self.last_battery_response = None self.request_in_progress = False + # Timestamp of last successful response from battery, + # climate or location. + self.last_battery_response = None + self.last_climate_response = None + self.last_location_response = None async def async_update_data(self, now): """Update data from nissan leaf.""" @@ -212,7 +217,8 @@ def get_next_interval(self): interval = min(intervals) _LOGGER.debug("Resulting interval=%s", interval) - return utcnow() + interval + self.next_update = utcnow() + interval + return self.next_update async def async_refresh_data(self, now): """Refresh the leaf data and update the datastore.""" @@ -255,6 +261,7 @@ async def async_refresh_data(self, now): _LOGGER.debug("Got climate data for Leaf: %s", climate_response.__dict__) self.data[DATA_CLIMATE] = climate_response.is_hvac_running + self.last_climate_response = utcnow() except CarwingsError: _LOGGER.error("Error fetching climate info") @@ -268,6 +275,7 @@ async def async_refresh_data(self, now): 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__) @@ -445,6 +453,18 @@ def log_registration(self): "Registered %s component for VIN %s", self.__class__.__name__, self.car.leaf.vin) + @property + def device_state_attributes(self): + """Return default attributes for Nissan leaf entities.""" + return { + 'next_update': self.car.next_update, + '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 + } + + async def async_added_to_hass(self): """Register callbacks.""" self.log_registration() diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index 20eced258e6f0..f6e0381cd2a6f 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -43,6 +43,14 @@ def log_registration(self): "Registered LeafClimateSwitch component with HASS for VIN %s", self.car.leaf.vin) + @property + def device_state_attributes(self): + """Return climate control attributes.""" + attrs = super(LeafClimateSwitch, self).device_state_attributes + attrs["updated_on"] = self.car.last_climate_response + return attrs + + @property def is_on(self): """Return true if climate control is on.""" From 09f4dc6a89daf9cf4a21567be041221c7ba6c290 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 9 Feb 2019 19:01:58 +0000 Subject: [PATCH 69/78] Add nissan_leaf.update service that triggers an update. --- .../components/nissan_leaf/__init__.py | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 0d5522beaa7ae..37b0c224592d8 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -84,11 +84,33 @@ SIGNAL_UPDATE_LEAF = 'nissan_leaf_update' +SERVICE_UPDATE_LEAF = 'update' +ATTR_VIN = 'vin' + +UPDATE_LEAF_SCHEMA = vol.Schema({ + vol.Required(ATTR_VIN): cv.string +}) + async def async_setup(hass, config): """Set-up the Nissan Leaf component.""" import pycarwings2 + async def handle_update(service): + # 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, '') + + 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 + + _LOGGER.debug("Vin %s not recognised for update.", vin) + return False + + async def async_setup_leaf(car_config): """Set-up a car.""" _LOGGER.debug("Logging into You+Nissan...") @@ -145,6 +167,9 @@ async def leaf_login(): if tasks: await asyncio.wait(tasks, loop=hass.loop) + hass.services.async_register(DOMAIN, SERVICE_UPDATE_LEAF, handle_update, + schema=UPDATE_LEAF_SCHEMA) + return True @@ -174,14 +199,24 @@ def __init__(self, leaf, hass, car_config): self.last_battery_response = None self.last_climate_response = None self.last_location_response = None + self._remove_listener = None async def async_update_data(self, now): """Update data from nissan leaf.""" + # Prevent against a previously scheduled update and an ad-hoc update + # started from an update from both being triggered. + if self._remove_listener: + self._remove_listener() + self._remove_listener = None + + # Clear next update whilst this update is underway + self.next_update = None + await self.async_refresh_data(now) - next_interval = self.get_next_interval() - _LOGGER.debug("Next interval=%s", next_interval) - async_track_point_in_utc_time( - self.hass, self.async_update_data, next_interval) + self.next_update = self.get_next_interval() + _LOGGER.debug("Next update=%s", self.next_update) + self._remove_listener = async_track_point_in_utc_time( + self.hass, self.async_update_data, self.next_update) def get_next_interval(self): """Calculate when the next update should occur.""" @@ -217,8 +252,7 @@ def get_next_interval(self): interval = min(intervals) _LOGGER.debug("Resulting interval=%s", interval) - self.next_update = utcnow() + interval - return self.next_update + return utcnow() + interval async def async_refresh_data(self, now): """Refresh the leaf data and update the datastore.""" @@ -436,8 +470,9 @@ async def async_start_charging(self): _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_refresh_data, check_charge_at) + self.hass, self.async_update_data, check_charge_at) class LeafEntity(Entity): From fe6876385e3b16bc5129e51ffacd812423f03037 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Sat, 9 Feb 2019 19:03:28 +0000 Subject: [PATCH 70/78] Flake8 line fixes --- homeassistant/components/nissan_leaf/__init__.py | 2 -- homeassistant/components/nissan_leaf/switch.py | 1 - 2 files changed, 3 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 37b0c224592d8..e74c64afb5473 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -110,7 +110,6 @@ async def handle_update(service): _LOGGER.debug("Vin %s not recognised for update.", vin) return False - async def async_setup_leaf(car_config): """Set-up a car.""" _LOGGER.debug("Logging into You+Nissan...") @@ -499,7 +498,6 @@ def device_state_attributes(self): 'location_updated_on': self.car.last_location_response } - async def async_added_to_hass(self): """Register callbacks.""" self.log_registration() diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index f6e0381cd2a6f..99b7f1ad241b4 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -50,7 +50,6 @@ def device_state_attributes(self): attrs["updated_on"] = self.car.last_climate_response return attrs - @property def is_on(self): """Return true if climate control is on.""" From c4ced4b14b56fe0fe65d3e5caedb3299dc15252d Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 11 Feb 2019 22:43:54 +0000 Subject: [PATCH 71/78] Remove excess and double logging. --- homeassistant/components/nissan_leaf/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index e74c64afb5473..6f3f13068ec2b 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -144,7 +144,6 @@ async def leaf_login(): sys.exc_info()[0]) return False - _LOGGER.info("Successfully logged in and fetched Leaf info") _LOGGER.warning( "WARNING: This may poll your Leaf too often, and drain the 12V" " battery. If you drain your cars 12V battery it WILL NOT START" @@ -495,7 +494,8 @@ def device_state_attributes(self): '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 + 'location_updated_on': self.car.last_location_response, + 'vin': self.car.leaf.vin, } async def async_added_to_hass(self): From 6f5c00a4e46f1a6068450b5fc5095f15496c77ee Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 11 Feb 2019 22:56:02 +0000 Subject: [PATCH 72/78] Add updated_on attribute for device tracker. --- homeassistant/components/nissan_leaf/device_tracker.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py index 40195d9a9576d..ab2a19ede54ec 100644 --- a/homeassistant/components/nissan_leaf/device_tracker.py +++ b/homeassistant/components/nissan_leaf/device_tracker.py @@ -34,12 +34,16 @@ def see_vehicle(): _LOGGER.debug("Updating device_tracker for %s with position %s", value.leaf.nickname, value.data[DATA_LOCATION].__dict__) + attrs = { + 'updated_on': value.last_location_response, + } see(dev_id=dev_id, host_name=host_name, gps=( value.data[DATA_LOCATION].latitude, value.data[DATA_LOCATION].longitude ), + attributes=attrs, icon=ICON_CAR) dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) From 85218d8bb98f6b7a049753f22790c67e312a123e Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Thu, 14 Feb 2019 21:14:34 +0000 Subject: [PATCH 73/78] Fix crash if pycarwings2 doesn't provide cruising ranges. --- .../components/nissan_leaf/__init__.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 6f3f13068ec2b..68e207374688d 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -273,12 +273,24 @@ async def async_refresh_data(self, now): if server_response.answer['status'] == 200: self.data[DATA_BATTERY] = server_response.battery_percent - self.data[DATA_RANGE_AC] = ( - server_response.cruising_range_ac_on_km - ) - self.data[DATA_RANGE_AC_OFF] = ( - server_response.cruising_range_ac_off_km - ) + + # pycarwings2 library doesn't always provide cruising rnages + # so we have to check if they exist before we can use them. + # Root cause: the nissan servers don't always send the data. + if hasattr(server_response, 'cruising_range_ac_on_km'): + self.data[DATA_RANGE_AC] = ( + server_response.cruising_range_ac_on_km + ) + else: + self.data[DATA_RANGE_AC] = None + + if hasattr(server_response, 'cruising_range_ac_off_km'): + self.data[DATA_RANGE_AC_OFF] = ( + server_response.cruising_range_ac_off_km + ) + else: + self.data[DATA_RANGE_AC_OFF] = None + self.data[DATA_PLUGGED_IN] = ( server_response.is_connected ) From 71c80dc6c32b323770b610a25db09f35906d4bb8 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 15 Feb 2019 12:55:42 +0100 Subject: [PATCH 74/78] Minor changes --- .../components/nissan_leaf/switch.py | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index 99b7f1ad241b4..914e85b48a66a 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -1,16 +1,10 @@ -""" -Charge and Climate Control Support for the Nissan Leaf. - -Please refer to the main platform component for configuration details -""" - +"""Charge and Climate Control Support for the Nissan Leaf.""" import logging + from homeassistant.components.nissan_leaf import ( - DATA_CHARGING, DATA_CLIMATE, DATA_LEAF, LeafEntity -) + DATA_CHARGING, DATA_CLIMATE, DATA_LEAF, LeafEntity) from homeassistant.helpers.entity import ToggleEntity - _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['nissan_leaf'] @@ -18,8 +12,8 @@ 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) + _LOGGER.debug( + "In switch setup platform, discovery_info=%s", discovery_info) devices = [] for value in hass.data[DATA_LEAF].values(): @@ -35,7 +29,7 @@ class LeafClimateSwitch(LeafEntity, ToggleEntity): @property def name(self): """Switch name.""" - return self.car.leaf.nickname + " Climate Control" + return "{} {}".format(self.car.leaf.nickname, "Climate Control") def log_registration(self): """Log registration.""" @@ -79,7 +73,7 @@ class LeafChargeSwitch(LeafEntity, ToggleEntity): @property def name(self): """Switch name.""" - return self.car.leaf.nickname + " Charging Status" + return "{} {}".format(self.car.leaf.nickname, "Charging Status") @property def icon(self): @@ -101,5 +95,5 @@ async def async_turn_on(self, **kwargs): 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.") + "Cannot turn off Leaf charging." + " Nissan API does not support stopping charge remotely") From 98229d20384105018cdc50bf211b972a9853e791 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 15 Feb 2019 12:56:14 +0100 Subject: [PATCH 75/78] Minor changes --- homeassistant/components/nissan_leaf/sensor.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 71474f8a422bb..3c8f9ab9ef324 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -1,17 +1,13 @@ -"""Battery Charge and Range Support for the Nissan Leaf. - -Please refer to the main platform component for configuration details -""" - +"""Battery Charge and Range Support for the Nissan Leaf.""" import logging -from homeassistant.const import DEVICE_CLASS_BATTERY -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES -from homeassistant.helpers.icon import icon_for_battery_level + from homeassistant.components.nissan_leaf import ( DATA_BATTERY, DATA_CHARGING, DATA_LEAF, DATA_RANGE_AC, DATA_RANGE_AC_OFF, - LeafEntity -) + LeafEntity) +from homeassistant.const import DEVICE_CLASS_BATTERY +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM _LOGGER = logging.getLogger(__name__) From 2ceb788f5d276d991c391bab03da7d445487dab3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 15 Feb 2019 12:56:38 +0100 Subject: [PATCH 76/78] Minor changes --- .../components/nissan_leaf/device_tracker.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py index ab2a19ede54ec..163675319eac8 100644 --- a/homeassistant/components/nissan_leaf/device_tracker.py +++ b/homeassistant/components/nissan_leaf/device_tracker.py @@ -1,15 +1,10 @@ -""" -Support for tracking a Nissan Leaf. - -For more details about this platform, please refer to the documentation -of the main platform component. -""" +"""Support for tracking a Nissan Leaf.""" import logging -from homeassistant.util import slugify -from homeassistant.helpers.dispatcher import dispatcher_connect + from homeassistant.components.nissan_leaf import ( - DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF -) + DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF) +from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) From db5d0b6dd9a374ede1e79a113d5d06d3ef358d81 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 15 Feb 2019 12:57:12 +0100 Subject: [PATCH 77/78] Minor changes --- .../components/nissan_leaf/binary_sensor.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index c8ffab8a48212..05255d616c43a 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Plugged In Status Support for the Nissan Leaf. - -Please refer to the main platform component for configuration details -""" - +"""Plugged In Status Support for the Nissan Leaf.""" import logging from homeassistant.components.nissan_leaf import ( @@ -15,14 +10,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - """Nissan Leaf binary_sensor setup.""" - _LOGGER.debug("binary_sensor setup_platform, discovery_info=%s", - discovery_info) + """Set up of a Nissan Leaf binary sensor.""" + _LOGGER.debug( + "binary_sensor setup_platform, discovery_info=%s", discovery_info) devices = [] for key, value in hass.data[DATA_LEAF].items(): - _LOGGER.debug("binary_sensor setup_platform, key=%s, value=%s", - key, value) + _LOGGER.debug( + "binary_sensor setup_platform, key=%s, value=%s", key, value) devices.append(LeafPluggedInSensor(value)) add_devices(devices, True) @@ -34,7 +29,7 @@ class LeafPluggedInSensor(LeafEntity): @property def name(self): """Sensor name.""" - return self.car.leaf.nickname + " Plug Status" + return "{} {}".format(self.car.leaf.nickname, "Plug Status") @property def state(self): From 5d79e4825f6650f495f2d26d9b49391983849034 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 15 Feb 2019 12:57:40 +0100 Subject: [PATCH 78/78] Minor changes --- .../components/nissan_leaf/__init__.py | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 68e207374688d..f5a8217242d9f 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -1,19 +1,14 @@ -""" -Support for the Nissan Leaf Carwings/Nissan Connect API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/nissan_leaf -""" - -import logging -from datetime import timedelta, datetime -import urllib +"""Support for the Nissan Leaf Carwings/Nissan Connect API.""" import asyncio +from datetime import datetime, timedelta +import logging import sys +import urllib + import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) @@ -67,13 +62,11 @@ vol.Optional(CONF_CHARGING_INTERVAL, default=DEFAULT_CHARGING_INTERVAL): ( vol.All(cv.time_period, - vol.Clamp( - min=MIN_UPDATE_INTERVAL))), + vol.Clamp(min=MIN_UPDATE_INTERVAL))), vol.Optional(CONF_CLIMATE_INTERVAL, default=DEFAULT_CLIMATE_INTERVAL): ( vol.All(cv.time_period, - vol.Clamp( - min=MIN_UPDATE_INTERVAL))), + vol.Clamp(min=MIN_UPDATE_INTERVAL))), vol.Optional(CONF_FORCE_MILES, default=False): cv.boolean })]) }, extra=vol.ALLOW_EXTRA) @@ -88,12 +81,12 @@ ATTR_VIN = 'vin' UPDATE_LEAF_SCHEMA = vol.Schema({ - vol.Required(ATTR_VIN): cv.string + vol.Required(ATTR_VIN): cv.string, }) async def async_setup(hass, config): - """Set-up the Nissan Leaf component.""" + """Set up the Nissan Leaf component.""" import pycarwings2 async def handle_update(service): @@ -103,15 +96,15 @@ async def handle_update(service): 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()) + async_track_point_in_utc_time( + hass, data_store.async_update_data, utcnow()) return True - _LOGGER.debug("Vin %s not recognised for update.", vin) + _LOGGER.debug("Vin %s not recognised for update", vin) return False async def async_setup_leaf(car_config): - """Set-up a car.""" + """Set up a car.""" _LOGGER.debug("Logging into You+Nissan...") username = car_config[CONF_USERNAME] @@ -125,7 +118,7 @@ async def leaf_login(): leaf = sess.get_leaf() try: - # this might need to be made async (somehow) causes + # 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): @@ -244,8 +237,8 @@ def get_next_interval(self): if self.data[DATA_CLIMATE]: intervals.append(climate_interval) - _LOGGER.debug("Could use climate interval=%s", - climate_interval) + _LOGGER.debug( + "Could use climate interval=%s", climate_interval) interval = min(intervals) _LOGGER.debug("Resulting interval=%s", interval)