Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nissan Leaf (Carwings/Nissan Connect EV) Platform #12116

Closed
wants to merge 17 commits into from

Conversation

BenWoodford
Copy link

@BenWoodford BenWoodford commented Feb 1, 2018

Description:

Adds support for monitoring and controlling the Nissan Leaf remote control platform.

Currently introduces two switches (climate control and charging, charging can only be turned on as per the weird API limitation), 3 sensors (battery charge and range with and without AC on) and a binary sensor for if the car is currently plugged in.

Further work needed at some point to add the device tracker, I have a 2015 Leaf so do not have this functionality on my car, right now the nissan_connect config option does not do anything as it is used to enable the device tracker.

Pull request in home-assistant.github.io with documentation (if applicable): documentation pending

Example entry for configuration.yaml (if applicable):

nissan_leaf:
  username: "username"
  password: "password"
  region: 'NE'
  update_interval: 30
  update_interval_charging: 15
  update_interval_climate: 5
  force_miles: true

Checklist:

  • The code change is tested and works locally.

If user exposed functionality or configuration variables are added/changed:

Pending docs.

If the code communicates with devices, web services, or third-party tools:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • [x ] New dependencies have been added to the REQUIREMENTS variable ([example][ex-requir]).
  • New dependencies are only imported inside functions that use them ([example][ex-import]).
  • New dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.

Currently using a GitHub URL, as the library's author is MIA and I had to fix up the library to get it working in HASS. Checked with @armills before submitting and have added a note to the code with the PR on pycarwings2 (jdhorne/pycarwings2#28)

  • New files were added to .coveragerc.

@homeassistant
Copy link
Contributor

Hi @BenWoodford,

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!


def turn_off(self, **kwargs):
_LOGGER.debug(
"Cannot turn off Leaf charging - Nissan does not support that remotely.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (85 > 79 characters)


@property
def is_on(self):
return self.car.data[LeafCore.DATA_CHARGING] == True

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comparison to True should be 'if cond is True:' or 'if cond:'


def log_registration(self):
_LOGGER.debug(
"Registered LeafChargeSwitch component with HASS for VIN " + self.car.leaf.vin)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (91 > 79 characters)


@property
def is_on(self):
return self.car.data[LeafCore.DATA_CLIMATE] == True

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comparison to True should be 'if cond is True:' or 'if cond:'


def log_registration(self):
_LOGGER.debug(
"Registered LeafClimateSwitch component with HASS for VIN " + self.car.leaf.vin)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (92 > 79 characters)


"""

self.signal_components()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IndentationError: unexpected indent
unexpected indentation



"""
# Removing this block for now, as I do not have a Leaf with Nissan Connect to test it with.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (91 > 79 characters)

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (83 > 79 characters)

" as climate control is on and the interval has passed.")
result = True

if result == True:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comparison to True should be 'if cond is True:' or 'if cond:'

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.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (83 > 79 characters)

@homeassistant
Copy link
Contributor

Hi @BenWoodford,

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!


@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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (128 > 79 characters)

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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (128 > 79 characters)


def log_registration(self):
_LOGGER.debug(
"Registered LeafRangeSensor component with HASS for VIN " + self.car.leaf.vin)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (90 > 79 characters)


def log_registration(self):
_LOGGER.debug(
"Registered LeafBatterySensor component with HASS for VIN " + self.car.leaf.vin)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (92 > 79 characters)

from .. import nissan_leaf as LeafCore
from ..nissan_leaf import LeafEntity
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.dispatcher import (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'homeassistant.helpers.dispatcher.async_dispatcher_connect' imported but unused
'homeassistant.helpers.dispatcher.dispatcher_send' imported but unused


import logging
from .. import nissan_leaf as LeafCore
from ..nissan_leaf import LeafEntity

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'..nissan_leaf.LeafEntity' imported but unused

"""
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (92 > 79 characters)

@@ -0,0 +1,88 @@
"""
Battery Charge and Range Support for the Nissan Leaf Carwings/Nissan Connect API.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (81 > 79 characters)


def _update_callback(self):
"""Callback update method."""
#_LOGGER.debug("Got dispatcher update from Leaf platform")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

block comment should start with '# '

_LOGGER.debug("Empty Location Response Received")
self.data[DATA_LOCATION] = None
else:
LOGGER.debug("Got location data for Leaf")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'LOGGER'

self.data[DATA_PLUGGED_IN] = False
self.last_check = None
track_time_interval(
hass, self.refresh_leaf_if_necessary, timedelta(seconds=CHECK_INTERVAL))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (84 > 79 characters)

@dedwards66
Copy link

Success! I got it installed onto my dev system. It appears to update sensors and just fine. I can turn on charging and climate control. I did not see the part about the tracker platform not being complete yet. I added it as a tracker to the config file anyway.

The only bug I see is the Leaf’s charge is listed as 2000% at 100% charge. While I would like that upgrade, I remember reading on a forum that the bigger batteries report in a different format. I will see if I can find that.

Also as note for your docs. I believe I read that the Nissan Leaf does not charge the 12 volt battery while it is plugged into a charger. It charges it from the main battery while driving and while parked unplugged. It also trickle charges the 12v from the solar panel if you have one. This would be a good question for the Nissan Leaf forums.

For those like me who are noobs with doing github branches here is what I did to get my development raspberry pi with hassbian to install this branch. Please don’t try this on a production install, I am a noob at this.

ssh into your Raspberry Pi

ssh -l pi
Default password is raspberry

$ sudo systemctl stop home-assistant@homeassistant.service
$ sudo su -s /bin/bash homeassistant
$ source /srv/homeassistant/bin/activate
$ pip3 install git+https://github.com/BenWoodford/home-assistant.git@nissanleaf
$ exit
$ sudo systemctl start home-assistant@homeassistant.service

leaf_sensors
leaf_switches

dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle)
dispatcher_send(hass, SIGNAL_UPDATE_LEAF)

return True

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs
no newline at end of file

icon='mdi:car')

dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle)
dispatcher_send(hass, SIGNAL_UPDATE_LEAF)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

value.data[DATA_LOCATION].longitude),
icon='mdi:car')

dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

host_name=host_name,
gps=(value.data[DATA_LOCATION].latitude,
value.data[DATA_LOCATION].longitude),
icon='mdi:car')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs
continuation line under-indented for visual indent

see(dev_id=dev_id,
host_name=host_name,
gps=(value.data[DATA_LOCATION].latitude,
value.data[DATA_LOCATION].longitude),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs
indentation contains mixed spaces and tabs
continuation line under-indented for visual indent

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]:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs
missing whitespace after ','


for key, value in hass.data[DATA_LEAF].items():
host_name = value.leaf.nickname
dev_id = 'nissan_leaf_{}'.format(slugify(host_name))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

"""Handle the reporting of the vehicle position."""

for key, value in hass.data[DATA_LEAF].items():
host_name = value.leaf.nickname

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

def see_vehicle():
"""Handle the reporting of the vehicle position."""

for key, value in hass.data[DATA_LEAF].items():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

_LOGGER.debug("Setting up Scanner (device_tracker) for Nissan Leaf")

def see_vehicle():
"""Handle the reporting of the vehicle position."""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle)
dispatcher_send(hass, SIGNAL_UPDATE_LEAF)

return True

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs
no newline at end of file

icon='mdi:car')

dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle)
dispatcher_send(hass, SIGNAL_UPDATE_LEAF)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

value.data[DATA_LOCATION].longitude),
icon='mdi:car')

dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

host_name=host_name,
gps=(value.data[DATA_LOCATION].latitude,
value.data[DATA_LOCATION].longitude),
icon='mdi:car')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs
continuation line under-indented for visual indent

see(dev_id=dev_id,
host_name=host_name,
gps=(value.data[DATA_LOCATION].latitude,
value.data[DATA_LOCATION].longitude),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs
indentation contains mixed spaces and tabs
continuation line under-indented for visual indent

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]:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs
missing whitespace after ','


for key, value in hass.data[DATA_LEAF].items():
host_name = value.leaf.nickname
dev_id = 'nissan_leaf_{}'.format(slugify(host_name))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

"""Handle the reporting of the vehicle position."""

for key, value in hass.data[DATA_LEAF].items():
host_name = value.leaf.nickname

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

def see_vehicle():
"""Handle the reporting of the vehicle position."""

for key, value in hass.data[DATA_LEAF].items():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

_LOGGER.debug("Setting up Scanner (device_tracker) for Nissan Leaf")

def see_vehicle():
"""Handle the reporting of the vehicle position."""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation contains tabs

@homeassistant
Copy link
Contributor

Hi @kurol,

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

from homeassistant.util import slugify
from homeassistant.helpers.dispatcher import (
dispatcher_connect, dispatcher_send)
from homeassistant.components.nissan_leaf import DATA_LEAF, SIGNAL_UPDATE_LEAF, DATA_LOCATION

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (93 > 79 characters)


import logging

from .. import nissan_leaf as LeafCore
Copy link
Member

@balloob balloob Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python uses snake_case for all variable name that are not classes or root level variables


_LOGGER.debug("Adding sensors")

for key, value in hass.data[LeafCore.DATA_LEAF].items():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not using key, so use values() instead of items()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you can tell, Python is not my first language :)


add_devices(devices, True)

return True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platforms don't have return values

# So using my fork for now.
# https://github.com/jdhorne/pycarwings2/pull/28

REQUIREMENTS = ['https://github.com/BenWoodford/pycarwings2/archive/master.zip'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not merge this until we can point it at the updated pycarwings2 lib on PyPi

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I'm still waiting on the author to merge the PR in - something about Python 2 support.

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate intervals with vol.All(cv.time_period, cv.positive_timedelta), it will allow people to put in numbers but also "01:00". The validator converts all these different inputs to timedelta. You can get the value of the timedelta with value.total_seconds().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we actually let anyone override the intervals? As developers we should pick the right interval and not bother our users with it. Especially we don't want to allow the users to be able to DDOS the vendor and us getting the blame.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also just saw the warning about emptying the 12v battery, yep definitely we should not allow users to pick their own interval. We should pick conservative intervals too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've given it very conservative intervals but left it open to people making their own if they have it plugged in more. Alternatively we could look at decreasing the rate of polling if the drive train battery is on a low charge as that's when the 12V is more at risk (the drive train battery will top up the 12V intermittently)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that idea very much.


_LOGGER.info("Successfully logged in and fetched Leaf info")
_LOGGER.info(
"WARNING: This may poll your Leaf too often, and drain the 12V."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not allow people to brick their car.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly if they've left the car idle and on a low charge for a very long time. No helping that but it's a good warning to have.


def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will have to add

if discovery_info is not None:
    return

to all platforms because the component is triggering a load of the platforms. Otherwise you will get issues when a user would add the leaf as a binary sensor platform.

'homebridge_model': 'Leaf'
}

@asyncio.coroutine
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've migrated to async/await syntax. Please drop this decorator and prefix async methods with async like async def async_added_to_hass . Replace any yield from with await.

battery_response = self.get_battery()
_LOGGER.debug("Got battery data for Leaf")

if battery_response.answer['status'] == 200:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you store the battery_response object in the data store? That way you don't have to juggle around with all the DATA_ constants.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you mean? I have lots of different responses from the car which is why I've got the constants.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just store the latest response of every type

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OH, I see what you mean. Yeah that does make sense

battery_status = self.leaf.get_status_from_update(request)
while battery_status is None:
_LOGGER.debug("Battery data not in yet.")
time.sleep(5)
Copy link
Member

@balloob balloob Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use sleep inside sync contexts as it blocks threads. It's ok to sleep within an async context, however, it seems weird to keep polling indefinitely? It should give up after a few times to make we don't break the 12v battery…

async def async_get_battery(self):
    request = await self.hass.async_add_job(self.leaf.request_update)
    for i in range(5):
        if i > 0:
            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:
            break

    return battery_status

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had been using async but was told that will block the main thread instead. Nissan's servers are really bad so they can be pretty sluggish sometimes.

The reason we sleep here is because the initial request will ask for the battery from the car, the requests in the while loop are then asking Nissan's servers if the car has updated them with the latest battery data. It's a pretty poor implementation by them - Tesla constantly phone home so you don't have these issues but Nissan's systems were coded by monkeys.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok to use async as long as you offload I/O to the executor (a thread worker pool). That is done in my example by passing the methods that do I/O (like request_update) to hass.async_add_job and awaiting that. Once the function is executed, the async context will resume.

Copy link
Member

@balloob balloob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is off to a great start! 👍

Left some comments, but also please:

Please rebase your branch on the latest dev as we no longer support Python 3.4. This should help with testing.

Travis reports a ton of linting issues, these need to be addressed

And let's get rid of configuring intervals so that people don't brick their cars.

@RoadkillUK
Copy link

RoadkillUK commented Mar 9, 2018

Hi, I had this working the other day, even set up 'heat car' from google assistant (IFTTT), but now it's not working anymore and I get this in the logfile.

2018-03-09 18:27:30 ERROR (SyncWorker_14) [homeassistant.util.package] Unable to install package https://github.com/BenWoodford/pycarwings2/archive/master.zip#pycarwings: Command "/usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-eais25g4/pycryptodome/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-38e046qm-record/install-record.txt --single-version-externally-managed --prefix  --compile --user --prefix=" failed with error code 1 in /tmp/pip-build-eais25g4/pycryptodome/
2018-03-09 18:27:30 ERROR (MainThread) [homeassistant.requirements] Not initializing nissan_leaf because could not install requirement https://github.com/BenWoodford/pycarwings2/archive/master.zip#pycarwings
2018-03-09 18:27:30 ERROR (MainThread) [homeassistant.setup] Setup failed for nissan_leaf: Could not install all requirements.

Any help is appreciated, thanks.

@pblgomez
Copy link

How can install this as a custom component?

@RoadkillUK
Copy link

RoadkillUK commented Mar 29, 2018

Hi @pblgomez , I think this thread is more for working on the plug-in, however, there is a thread on HomeAssistant forum. Here is a link that should help you get installed.

Go to the bottom, there's a post from me.

https://community.home-assistant.io/t/nissan-leaf-component-s-platform/38663

Odd, that link goes elsewhere :/ Copy the one below

https://community.home-assistant.io/t/nissan-leaf-component-s-platform/38663

@balloob
Copy link
Member

balloob commented Mar 31, 2018

This PR seems to have gone stale. Closing it. You can reopen it when you're ready to finish it.

@balloob balloob closed this Mar 31, 2018
@home-assistant home-assistant locked and limited conversation to collaborators Jul 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants