Nissan Leaf Integration (Carwings / NissanConnect EV) (#19786)

* Added work so far.

* Change interval so nobody drains their battery when I put this online

* Added the warning notice.

* Async setup

* Still broken, but we're getting there.

* Back to synchronous, moved refresh stuff into DataStore

* Functional sensors!

* Added working switches, tweaked intervals a bit

* Fixed turn off result

* Moved plug status to binary_sensor, added smart intervals

* Documentation and car nickname stuff

* Syntax fixes and coveragerc additions

* Style fixes

* Fixing the final line length

* Fixed an issue with newer models and bad climate data

* Forgot to check my line endings.

* New icons for most of the components

* Hotfix for handling Nissan's awful servers

* 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

* Add pycarwings2 to requirements_all.txt

* Make stopping charge error an 'info'. Remove TODO.

* Request update from car after sending start charging command.

* Delay initial (slow) update for 15 seconds and make async

* Flake8 line length fixes

* Try to fix D401 'imperative mood' git diff tox errors

* Try to fix more D401 'imperative mood' tox errors

* Default interval of an hour in code, to match comments.

* Update to pycarwings2 2.3

* Update to pycarwings2 2.3 in requirements_all.txt

* Remove documentation, instead refering to home-assistant.io

* Remove unneeded dispatcher_send()

* Remove unneeded requirements comments

* Combine excess debugging.

* Remove single line method signal_components()

* Bump to version 2.4 of pycarwings2

* Remove unused dispatcher_send

* Simplify logging of LeafEntity registration

* Update requirements_all.txt

* Multiple changes

Increase timeout to 30 seconds
Only consider battery_status
Fix plugged in status
Better attempts at try/exception handling

* Fix line length

* Use pycarwings 2.5

* Remove pointless 'is True'

* Remove unnecessary 'is True/False'

* Remove unnecessary 'is True/False'

* Use LENGTH_MILES and LENGTH_KILOMETERS

* Remove excess logging in setup_platform()

* Remove unnecessary 'is True'

* Use pycarwings2 version 2.6

* Require pycarwings2 version 2.7.

* Increase sleep delay for climate and location reponses.

* Remove unnecessary 'is True'

* Increase frequent polling warning to _LOGGER.warning()

* Use DEVICE_CLASS_BATTERY

* Remove extraneous 'is True'.

* Move icon strings to constants.

* Remove unneeded key.

* LeafRangeSensor ac_on property is internal.

* Flake8 missing line

* Remove homebridge attributes.

* Remove round battery % and range to whole numbers

* Use pycarwings2 2.8

* Move to embedded component model

* Reduce maximum attempts to 10 (5 mins)

* Include attempt count in 'waiting' log message

* Use await instead of yield. Remove @asyncio.coroutine decorators.

* Add @filcole as nissan_leaf codeowner

* Fix checking for if not data returned from vehicle. Don't double send signal on location update.

* Exposed updated_on, update_in_progress and next_update attributes.

* Add nissan_leaf.update service that triggers an update.

* Flake8 line fixes

* Remove excess and double logging.

* Add updated_on attribute for device tracker.

* Fix crash if pycarwings2 doesn't provide cruising ranges.

* Minor changes

* Minor changes

* Minor changes

* Minor changes

* Minor changes
pull/21094/head
Phil Cole 2019-02-15 13:35:26 +00:00 committed by Fabian Affolter
parent 7b19428279
commit 656d39e3ec
8 changed files with 821 additions and 0 deletions

View File

@ -327,6 +327,7 @@ omit =
homeassistant/components/nest/* homeassistant/components/nest/*
homeassistant/components/netatmo/* homeassistant/components/netatmo/*
homeassistant/components/netgear_lte/* homeassistant/components/netgear_lte/*
homeassistant/components/nissan_leaf/*
homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py

View File

@ -224,6 +224,7 @@ homeassistant/components/*/mystrom.py @fabaff
# N # N
homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/ness_alarm/* @nickw444
homeassistant/components/*/ness_alarm.py @nickw444 homeassistant/components/*/ness_alarm.py @nickw444
homeassistant/components/nissan_leaf/* @filcole
# O # O
homeassistant/components/openuv/* @bachya homeassistant/components/openuv/* @bachya

View File

@ -0,0 +1,514 @@
"""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.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
REQUIREMENTS = ['pycarwings2==2.8']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'nissan_leaf'
DATA_LEAF = 'nissan_leaf_data'
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'
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'
INITIAL_UPDATE = timedelta(seconds=15)
MIN_UPDATE_INTERVAL = timedelta(minutes=2)
DEFAULT_INTERVAL = timedelta(hours=1)
DEFAULT_CHARGING_INTERVAL = timedelta(minutes=15)
DEFAULT_CLIMATE_INTERVAL = timedelta(minutes=5)
RESTRICTED_BATTERY = 2
RESTRICTED_INTERVAL = timedelta(hours=12)
MAX_RESPONSE_ATTEMPTS = 10
PYCARWINGS2_SLEEP = 30
CONFIG_SCHEMA = 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): (
vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))),
vol.Optional(CONF_CHARGING_INTERVAL,
default=DEFAULT_CHARGING_INTERVAL): (
vol.All(cv.time_period,
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.Optional(CONF_FORCE_MILES, default=False): cv.boolean
})])
}, extra=vol.ALLOW_EXTRA)
LEAF_COMPONENTS = [
'sensor', 'switch', 'binary_sensor', 'device_tracker'
]
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...")
username = car_config[CONF_USERNAME]
password = car_config[CONF_PASSWORD]
region = car_config[CONF_REGION]
leaf = None
async def leaf_login():
nonlocal leaf
sess = pycarwings2.Session(username, password, region)
leaf = sess.get_leaf()
try:
# This might need to be made async (somehow) causes
# homeassistant to be slow to start
await hass.async_add_job(leaf_login)
except(RuntimeError, urllib.error.HTTPError):
_LOGGER.error(
"Unable to connect to Nissan Connect with "
"username and password")
return False
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.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."
" Don't set the intervals too low.")
data_store = LeafDataStore(leaf, hass, car_config)
hass.data[DATA_LEAF][leaf.vin] = data_store
for component in LEAF_COMPONENTS:
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,
utcnow() + INITIAL_UPDATE)
hass.data[DATA_LEAF] = {}
tasks = [async_setup_leaf(car) for car in config[DOMAIN]]
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(DOMAIN, SERVICE_UPDATE_LEAF, handle_update,
schema=UPDATE_LEAF_SCHEMA)
return True
class LeafDataStore:
"""Nissan Leaf Data Store."""
def __init__(self, leaf, hass, car_config):
"""Initialise the data store."""
self.leaf = leaf
self.car_config = car_config
self.nissan_connect = car_config[CONF_NCONNECT]
self.force_miles = car_config[CONF_FORCE_MILES]
self.hass = hass
self.data = {}
self.data[DATA_CLIMATE] = False
self.data[DATA_BATTERY] = 0
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.next_update = None
self.last_check = 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
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)
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."""
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
async def async_refresh_data(self, now):
"""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
_LOGGER.debug("Updating Nissan Leaf Data")
self.last_check = datetime.today()
self.request_in_progress = True
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:
self.data[DATA_BATTERY] = server_response.battery_percent
# 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
)
async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF)
self.last_battery_response = utcnow()
# Climate response only updated if battery data updated first.
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
self.last_climate_response = utcnow()
except CarwingsError:
_LOGGER.error("Error fetching climate info")
if self.nissan_connect:
try:
location_response = await self.async_get_location()
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] = location_response
self.last_location_response = utcnow()
_LOGGER.debug("Location Response: %s",
location_response.__dict__)
except CarwingsError:
_LOGGER.error("Error fetching location info")
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."""
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
)
# 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 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
# 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."""
from pycarwings2 import CarwingsError
try:
request = await self.hass.async_add_job(
self.leaf.get_latest_hvac_status
)
return request
except CarwingsError:
_LOGGER.error(
"An error occurred communicating with the car %s",
self.leaf.vin)
return None
async def async_set_climate(self, toggle):
"""Set climate control mode via Nissan servers."""
climate_result = None
if toggle:
_LOGGER.info("Requesting climate turn on for %s", self.leaf.vin)
request = await self.hass.async_add_job(
self.leaf.start_climate_control
)
for attempt in range(MAX_RESPONSE_ATTEMPTS):
if attempt > 0:
_LOGGER.info("Climate data not in yet (%s) (%s). "
"Waiting (%s) seconds.", self.leaf.vin,
attempt, PYCARWINGS2_SLEEP)
await asyncio.sleep(PYCARWINGS2_SLEEP)
climate_result = await self.hass.async_add_job(
self.leaf.get_start_climate_control_result, request
)
if climate_result is not None:
break
else:
_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 %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
)
if climate_result is not None:
break
if climate_result is not None:
_LOGGER.debug("Climate result: %s", climate_result.__dict__)
async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF)
return climate_result.is_hvac_running == toggle
_LOGGER.debug("Climate result not returned by Nissan servers")
return False
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 %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
)
if location_status is not None:
_LOGGER.debug("Location_status=%s", location_status.__dict__)
break
return location_status
async def async_start_charging(self):
"""Request start charging via Nissan servers."""
# Send the command to request charging is started to Nissan servers.
# If that completes OK then trigger a fresh update to pull the
# charging status from the car after waiting a minute for the
# charging request to reach the car.
result = await self.hass.async_add_job(self.leaf.start_charging)
if result:
_LOGGER.debug("Start charging sent, "
"request updated data in 1 minute")
check_charge_at = utcnow() + timedelta(minutes=1)
self.next_update = check_charge_at
async_track_point_in_utc_time(
self.hass, self.async_update_data, check_charge_at)
class LeafEntity(Entity):
"""Base class for Nissan Leaf entity."""
def __init__(self, car):
"""Store LeafDataStore upon init."""
self.car = car
def log_registration(self):
"""Log registration."""
_LOGGER.debug(
"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,
'vin': self.car.leaf.vin,
}
async def async_added_to_hass(self):
"""Register callbacks."""
self.log_registration()
async_dispatcher_connect(
self.car.hass, SIGNAL_UPDATE_LEAF, self._update_callback)
def _update_callback(self):
"""Update the state."""
self.schedule_update_ha_state(True)

View File

@ -0,0 +1,44 @@
"""Plugged In Status Support for the Nissan Leaf."""
import logging
from homeassistant.components.nissan_leaf import (
DATA_LEAF, DATA_PLUGGED_IN, LeafEntity)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['nissan_leaf']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""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)
devices.append(LeafPluggedInSensor(value))
add_devices(devices, True)
class LeafPluggedInSensor(LeafEntity):
"""Plugged In Sensor class."""
@property
def name(self):
"""Sensor name."""
return "{} {}".format(self.car.leaf.nickname, "Plug Status")
@property
def state(self):
"""Return true if plugged in."""
return self.car.data[DATA_PLUGGED_IN]
@property
def icon(self):
"""Icon handling."""
if self.car.data[DATA_PLUGGED_IN]:
return 'mdi:power-plug'
return 'mdi:power-plug-off'

View File

@ -0,0 +1,46 @@
"""Support for tracking a Nissan Leaf."""
import logging
from homeassistant.components.nissan_leaf import (
DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF)
from homeassistant.helpers.dispatcher import dispatcher_connect
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['nissan_leaf']
ICON_CAR = "mdi:car"
def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Nissan Leaf tracker."""
_LOGGER.debug("Setting up Scanner (device_tracker) for Nissan Leaf, "
"discovery_info=%s", discovery_info)
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 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].__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)
return True

View File

@ -0,0 +1,113 @@
"""Battery Charge and Range Support for the Nissan Leaf."""
import logging
from homeassistant.components.nissan_leaf import (
DATA_BATTERY, DATA_CHARGING, DATA_LEAF, DATA_RANGE_AC, DATA_RANGE_AC_OFF,
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__)
DEPENDENCIES = ['nissan_leaf']
ICON_RANGE = 'mdi:speedometer'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Sensors setup."""
_LOGGER.debug("setup_platform nissan_leaf sensors, discovery_info=%s",
discovery_info)
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)
class LeafBatterySensor(LeafEntity):
"""Nissan Leaf Battery Sensor."""
@property
def name(self):
"""Sensor Name."""
return self.car.leaf.nickname + " Charge"
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_BATTERY
@property
def state(self):
"""Battery state percentage."""
return round(self.car.data[DATA_BATTERY])
@property
def unit_of_measurement(self):
"""Battery state measured in percentage."""
return '%'
@property
def icon(self):
"""Battery state icon handling."""
chargestate = self.car.data[DATA_CHARGING]
return icon_for_battery_level(
battery_level=self.state,
charging=chargestate
)
class LeafRangeSensor(LeafEntity):
"""Nissan Leaf Range Sensor."""
def __init__(self, car, ac_on):
"""Set-up range sensor. Store if 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:
return self.car.leaf.nickname + " Range (AC)"
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."""
if self._ac_on:
ret = self.car.data[DATA_RANGE_AC]
else:
ret = self.car.data[DATA_RANGE_AC_OFF]
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)
@property
def unit_of_measurement(self):
"""Battery range unit."""
if (not self.car.hass.config.units.is_metric or
self.car.force_miles):
return LENGTH_MILES
return LENGTH_KILOMETERS
@property
def icon(self):
"""Nice icon for range."""
return ICON_RANGE

View File

@ -0,0 +1,99 @@
"""Charge and Climate Control Support for the Nissan Leaf."""
import logging
from homeassistant.components.nissan_leaf import (
DATA_CHARGING, DATA_CLIMATE, DATA_LEAF, LeafEntity)
from homeassistant.helpers.entity import ToggleEntity
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['nissan_leaf']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Nissan Leaf switch platform setup."""
_LOGGER.debug(
"In switch setup platform, discovery_info=%s", discovery_info)
devices = []
for value in hass.data[DATA_LEAF].values():
devices.append(LeafChargeSwitch(value))
devices.append(LeafClimateSwitch(value))
add_devices(devices, True)
class LeafClimateSwitch(LeafEntity, ToggleEntity):
"""Nissan Leaf Climate Control switch."""
@property
def name(self):
"""Switch name."""
return "{} {}".format(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 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."""
return self.car.data[DATA_CLIMATE]
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
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):
"""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")

View File

@ -949,6 +949,9 @@ pyblackbird==0.5
# homeassistant.components.neato # homeassistant.components.neato
pybotvac==0.0.13 pybotvac==0.0.13
# homeassistant.components.nissan_leaf
pycarwings2==2.8
# homeassistant.components.cloudflare # homeassistant.components.cloudflare
pycfdns==0.0.1 pycfdns==0.0.1