From af3da49ada2c66ffcc88e770ce343ac0ee3e5673 Mon Sep 17 00:00:00 2001 From: Reto Schuettel Date: Sun, 27 Oct 2019 23:25:35 +0100 Subject: [PATCH] allow exporting trips/routes to a folder --- .gitignore | 1 + voc | 8 +++- volvooncall/volvooncall.py | 93 +++++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b38063d..2ca4ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ lib pip-selfcheck.json .pytest_cache pytype_output +local diff --git a/voc b/voc index 8c8b941..8f28ec4 100755 --- a/voc +++ b/voc @@ -9,6 +9,7 @@ Usage: voc [-v|-vv] [options] list voc [-v|-vv] [options] status voc [-v|-vv] [options] trips [(--pretty|--json|--csv)] + voc [-v|-vv] [options] export-trips voc [-v|-vv] [options] owntracks voc [-v|-vv] [options] print [] voc [-v|-vv] [options] (lock | unlock) @@ -148,7 +149,7 @@ async def main(args): return await mqtt.run(connection, config) - journal = args["trips"] or args["dashboard"] or args["mqtt"] + journal = args["trips"] or args["dashboard"] or args["mqtt"] or args['export-trips'] res = await connection.update(journal=journal) if not res: @@ -192,7 +193,10 @@ async def main(args): trip["endPosition"]["city"], ) ) - + elif args["export-trips"]: + export_folder = args[''] + exported, skipped = await connection.export_trips(folder=export_folder) + print("Exported %i trips, %i trips already existed (use -v for more details)" % (exported, skipped)) elif args["print"]: attr = args[""] if attr: diff --git a/volvooncall/volvooncall.py b/volvooncall/volvooncall.py index 1dbb2ce..4b6a9b4 100755 --- a/volvooncall/volvooncall.py +++ b/volvooncall/volvooncall.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Communicate with VOC server.""" - +import json import logging from datetime import timedelta from json import dumps as to_json from collections import OrderedDict +from os import makedirs, rename +from os.path import join, exists from sys import argv from urllib.parse import urljoin import asyncio +import itertools +from random import randint from aiohttp import ClientSession, ClientTimeout, BasicAuth from aiohttp.hdrs import METH_GET, METH_POST @@ -28,6 +32,10 @@ SERVICE_URL = "https://vocapi{region}.wirelesscar.net/customerapi/rest/v3.0/" DEFAULT_SERVICE_URL = SERVICE_URL.format(region="") +# 5 days * 24 hours * 60 minutes * 5 waypoints per minute / 1000 pages +# => 36 pages, should be sufficent +MAX_PAGE_COUNTER_ROUTES = 36 + HEADERS = { "X-Device-Id": "Device", "X-OS-Type": "Android", @@ -124,6 +132,89 @@ async def update_vehicle(self, vehicle, journal=False): if journal: self._state[url].update(await self.get("trips", rel=url)) + async def export_trips(self, folder): + """ + export trips/routes of all vehicles to the given folder + + update(journal=true) must be called before calling this method + """ + exported = 0 + skipped = 0 + for vehicle in self.vehicles: + this_exported, this_skipped = await self.export_trips_of_vehicle(vehicle, folder=folder) + exported += this_exported + skipped += this_skipped + return exported, skipped + + async def export_trips_of_vehicle(self, vehicle, folder): + exported = 0 + skipped = 0 + out_path = join(folder, vehicle.vin) + makedirs(out_path, exist_ok=True) + + for trip in vehicle.trips: + was_exported = await self.export_single_trip_to_folder(vehicle, trip, out_path) + if was_exported: + exported += 1 + else: + skipped += 1 + + return exported, skipped + + async def export_single_trip_to_folder(self, vehicle, trip, out_path): + trip_start_time = trip['tripDetails'][0]['startTime'] + trip_ident = "%s_%s" % (trip_start_time.isoformat(), trip['id']) + trip_folder = join(out_path, trip_ident) + + if exists(trip_folder): + _LOGGER.info("EXPORT: SKIP %s: is already exported" % trip_ident) + return False + + # first generate into a temp folder + trip_folder_tmp = trip_folder + "-tmp-%x" % (randint(0, 10000)) + makedirs(trip_folder_tmp) + + trip_file = join(trip_folder_tmp, "trip.json") + waypoints_file = join(trip_folder_tmp, "waypoints.json") + + await self.write_trip_file(trip_file, trip) + + if 'routeDetails' in trip: + waypoints_count = await self.write_waypoints_file(vehicle, trip, waypoints_file) + else: + waypoints_count = 0 + + rename(trip_folder_tmp, trip_folder) + _LOGGER.info(("EXPORT: DONE %s: exported (%i waypoints)" % (trip_ident, waypoints_count))) + return True + + async def write_waypoints_file(self, vehicle, trip, waypoints_file): + with open(waypoints_file, "w") as fh: + waypoints = await self._fetch_waypoints_for_trip(trip['id'], vehicle._url) + json.dump(waypoints, fh, indent=4, sort_keys=True, default=str) + return len(waypoints) + + async def write_trip_file(self, trip_file, trip): + with open(trip_file, "w") as fh: + json.dump(trip, fh, indent=4, sort_keys=True, default=str) + + async def _fetch_waypoints_for_trip(self, trip_id, rel_url): + waypoints = [] + for page in itertools.count(): + route_url = "trips/%i/route?page=%i&page_size=1000" % (trip_id, page) + route_details_page = await self.get(route_url, rel=rel_url) + + this_page_waypoints = route_details_page['waypoints'] + if len(this_page_waypoints) == 0: + break + + if page > MAX_PAGE_COUNTER_ROUTES: + raise RuntimeError("Fetched more than %i pages, something is broken for %s" % ( + page, route_url)) + + waypoints.extend((this_page_waypoints)) + return waypoints + @property def vehicles(self): """Return vehicle state."""