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

simpified data contributions #50

Merged
merged 6 commits into from
Mar 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ bimmer_connected
This is a simple library to query and control the status of your BMW from
the connected drive portal.

See status.py and remote_light_flash.py for usage instruction or the
See :code:`bimmerconnected` for usage instruction or the
`API Documentation <http://bimmer-connected.readthedocs.io/en/latest/>`_.

I wrote this library as I want to include it in Home Assistant.


Compatibility
-------------
So far it is tested on vehicles with a 'NBTEvo', 'EntryEvo', 'NBT', or 'EntryNav' navigation system.
This works with BMW vehicles with a Connected Driver account.
So far it is tested on vehicles with a 'NBTEvo', 'EntryEvo', 'NBT', or 'EntryNav' navigation system.
If you have any trouble with other navigation systems, please create an issue
with your server responses (see next section).

Expand All @@ -29,17 +30,26 @@ If you want to contribute your data, perform the following steps:

::

git clone https://github.com/ChristianKuehnel/bimmer_connected.git
cd bimmer_connected
./generate_vehicle_fingerprint.py <username> <password> <country>
# get the latest version of the library
pip3 install --upgrade https://github.com/ChristianKuehnel/bimmer_connected/archive/b2vapi.zip

This will create a set of log files in the "vehicle_fingerprint" folder.
Before sending the data to anyone please **remove any personal data** from it:

* Replace your vehicle identification number (VIN) with something else like "my_vin"
* Replace the location of your vehicle (gps_lat, gps_lng) with some generic numbers e.g. 11.111
# run the fingerprint function
bimmerconnected fingerprint <username> <password> <region> <vin>

We will then use this data as additional test cases. So we will publish
This will create a set of log files in the "vehicle_fingerprint" folder.
Before sending the data to anyone please **check for any personal data**.
The following attributes should be replaced with default values:
* vin (=Vehicle Identification Number)
* lat and lon (=GPS position)
* licensePlate

Create a new
`issue in bimmer_connected <https://github.com/ChristianKuehnel/bimmer_connected/issues>`_
and
`add the files as attachment <https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/>`_
to the issue.

**Note:** We will then use this data as additional test cases. So we will publish
(parts of) it (after checking for personal information again) and use
this as test cases for our library. If you do not want this, please
let us know in advance.
Expand All @@ -57,7 +67,7 @@ And please add tests where it makes sense. The more the better.
Thank you
---------

Thank you @gerard33 and @m1n3rva for your research and contributions!
Thank you @gerard33, @m1n3rva, @kernelkraut, @robbz23, @lawtancool for your research and contributions!

This library is basically a best-of of other similar solutions I found,
yet none of them provided a ready to use library with a matching interface
Expand All @@ -69,6 +79,10 @@ to be used in Home Assistant and is available on pypi...

Thank you for your great software!

License
-------
The bimmer_connected library is licensed under the Apache License 2.0.

Disclaimer
----------
This library is not affiliated with or endorsed by BMW Group.
38 changes: 35 additions & 3 deletions bimmer_connected/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import logging
import urllib
import os
import json
from threading import Lock
from typing import Callable, List
import requests
Expand Down Expand Up @@ -129,12 +130,43 @@ def send_request(self, url: str, data=None, headers=None, expected_response=200,
return response

def _log_response_to_file(self, response: requests.Response, logfilename: str = None) -> None:
"""If a log path is set, log all resonses to a file"""
"""If a log path is set, log all resonses to a file."""
if self._log_responses is None or logfilename is None:
return

with open(os.path.join(self._log_responses, '{}.json'.format(logfilename)), 'w') as logfile:
logfile.write(response.text)
anonymized_data = json.dumps(self._anonymize_data(response.json()), indent=2, sort_keys=True)

output_path = None
count = 0

while output_path is None or os.path.exists(output_path):
output_path = os.path.join(self._log_responses, '{}_{}.txt'.format(logfilename, count))
count += 1

with open(output_path, 'w') as logfile:
logfile.write(anonymized_data)

@staticmethod
def _anonymize_data(json_data: dict) -> dict:
"""Replace parts of the logfiles containing personal information."""

replacements = {
'lat': 12.3456,
'lon': 34.5678,
'heading': 123,
'vin': 'some_vin',
'licensePlate': 'some_license_plate',
}

for key, value in json_data.items():
if key in replacements:
json_data[key] = replacements[key]
if isinstance(value, dict):
json_data[key] = ConnectedDriveAccount._anonymize_data(value)
if isinstance(value, list):
json_data[key] = [ConnectedDriveAccount._anonymize_data(v) for v in value]

return json_data

@property
def server_url(self) -> str:
Expand Down
90 changes: 90 additions & 0 deletions bimmerconnected
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/python3
"""Simple executable to demonstrate and test the usage of the library."""

import argparse
import logging
import json
import os
import shutil
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name, valid_regions

FINGERPRINT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'vehicle_fingerprint')


def main() -> None:
"""Main function."""
logging.basicConfig(level=logging.DEBUG)

parser = argparse.ArgumentParser(description='demo script to show usage of the bimmer_connected library')
subparsers = parser.add_subparsers(dest='cmd')
subparsers.required = True

status_parser = subparsers.add_parser('status', description='get current status of the vehicle')
_add_default_arguments(status_parser)
status_parser.set_defaults(func=get_status)

fingerprint_parser = subparsers.add_parser('fingerprint', description='save a vehicle fingerprint')
_add_default_arguments(fingerprint_parser)
fingerprint_parser.set_defaults(func=fingerprint)

flash_parser = subparsers.add_parser('lightflash', description='flash the vehicle lights')
_add_default_arguments(flash_parser)
flash_parser.add_argument('vin', help='vehicle identification number')
flash_parser.set_defaults(func=light_flash)

args = parser.parse_args()
args.func(args)


def get_status(args) -> None:
"""Get the vehicle status."""
account = ConnectedDriveAccount(args.username, args.password, get_region_from_name(args.region))
account.update_vehicle_states()

print('Found {} vehicles: {}'.format(
len(account.vehicles),
','.join([v.name for v in account.vehicles])))

for vehicle in account.vehicles:
print('VIN: {}'.format(vehicle.vin))
print('mileage: {}'.format(vehicle.state.mileage))
print('vehicle properties:')
print(json.dumps(vehicle.attributes, indent=4))
print('vehicle status:')
print(json.dumps(vehicle.state.attributes, indent=4))


def fingerprint(args) -> None:
"""Save the vehicle fingerprint."""
if os.path.exists(FINGERPRINT_DIR):
shutil.rmtree(FINGERPRINT_DIR)

os.mkdir(FINGERPRINT_DIR)

account = ConnectedDriveAccount(args.username, args.password, get_region_from_name(args.region),
log_responses=FINGERPRINT_DIR)
account.update_vehicle_states()

print('fingerprint of the vehicles written to {}'.format(FINGERPRINT_DIR))


def light_flash(args) -> None:
"""Trigger the vehicle to flash its lights."""
account = ConnectedDriveAccount(args.username, args.password, get_region_from_name(args.region))
vehicle = account.get_vehicle(args.vin)

status = vehicle.remote_services.trigger_remote_light_flash()
print(status.state)


def _add_default_arguments(status_parser: argparse.ArgumentParser):
"""Add the default arguments username, password, region to the parser."""
status_parser.add_argument('username', help='Connected Drive user name')
status_parser.add_argument('password', help='Connected Drive password')
status_parser.add_argument('region', choices=valid_regions(), help='Region of the Connected Drive account')


if __name__ == '__main__':
main()
40 changes: 0 additions & 40 deletions generate_vehicle_fingerprint.py

This file was deleted.

30 changes: 0 additions & 30 deletions remote_light_flash.py

This file was deleted.

3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ def readme():
install_requires=['requests', 'typing>=3,<4'],
keywords='BMW Connected Drive home automation',
zip_safe=False,
extras_require={'testing': ['pytest']}
extras_require={'testing': ['pytest']},
scripts=['bimmerconnected'],
)
38 changes: 0 additions & 38 deletions status.py

This file was deleted.

26 changes: 26 additions & 0 deletions test/test_account.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for ConnectedDriveAccount."""
import json
import unittest
from unittest import mock
from test import BackendMock, G31_VIN
Expand Down Expand Up @@ -42,3 +43,28 @@ def test_us_header(self):
ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, Regions.NORTH_AMERICA)
request = [r for r in backend_mock.last_request if 'oauth' in r.url][0]
self.assertEqual('b2vapi.bmwgroup.us', request.headers['Host'])

def test_anonymize_data(self):
"""Test anonymization function."""
test_dict = {
'vin': 'secret',
'a sub-dict': {
'lat': 666,
'lon': 666,
'heading': 666,
},
'licensePlate': 'secret',
'public': 'public_data',
'a_list': [
{'vin': 'secret'},
{
'lon': 666,
'public': 'more_public_data',
},
]
}
anon_text = json.dumps(ConnectedDriveAccount._anonymize_data(test_dict))
self.assertNotIn('secret', anon_text)
self.assertNotIn('666', anon_text)
self.assertIn('public_data', anon_text)
self.assertIn('more_public_data', anon_text)
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ commands=flake8
[testenv:pylint]
basepython = python3
skip_install = true
commands = pylint -j4 bimmer_connected test status.py remote_light_flash.py setup.py generate_vehicle_fingerprint.py
commands = pylint -j4 bimmer_connected test bimmerconnected

[testenv:docs]
basepython = python3
Expand Down