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
Conversation
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.") |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.") |
There was a problem hiding this comment.
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)
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: |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 ( |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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)
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 $ sudo systemctl stop home-assistant@homeassistant.service |
dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) | ||
dispatcher_send(hass, SIGNAL_UPDATE_LEAF) | ||
|
||
return True |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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]: |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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.""" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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]: |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indentation contains tabs
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 |
There was a problem hiding this comment.
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)
e11599f
to
170fa1a
Compare
|
||
import logging | ||
|
||
from .. import nissan_leaf as LeafCore |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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()
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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." |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = [] | ||
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
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.
Any help is appreciated, thanks. |
How can install this as a custom component? |
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 |
This PR seems to have gone stale. Closing it. You can reopen it when you're ready to finish it. |
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):Checklist:
If user exposed functionality or configuration variables are added/changed:
Pending docs.
If the code communicates with devices, web services, or third-party tools:
tox
run successfully. Your PR cannot be merged unless tests passREQUIREMENTS
variable ([example][ex-requir]).requirements_all.txt
by runningscript/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).coveragerc
.