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