Skip to content

Commit

Permalink
Merge pull request #190 from robinostlund/rate-limit
Browse files Browse the repository at this point in the history
Tweak own limits and try to handle 429 from server
  • Loading branch information
milkboy authored Apr 18, 2022
2 parents dc3e936 + 27ddbdb commit 1109b22
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 97 deletions.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ aiohttp
beautifulsoup4
lxml
pyjwt
pyrate_limiter>=2.6.0
pyrate-limiter>=2.6.0
25 changes: 12 additions & 13 deletions tests/vw_vehicle_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Vehicle class tests."""
import sys
from datetime import datetime, timezone, timedelta

import pytest
import sys

from volkswagencarnet.vw_const import VehicleStatusParameter as P
from volkswagencarnet.vw_timer import TimerData
from volkswagencarnet.vw_utilities import json_loads
from .fixtures.connection import TimersConnection, TimersConnectionNoSettings
Expand All @@ -28,9 +29,7 @@ class IsolatedAsyncioTestCase(TestCase):

from volkswagencarnet.vw_vehicle import (
Vehicle,
PRIMARY_DRIVE,
ENGINE_TYPE_ELECTRIC,
SECONDARY_DRIVE,
ENGINE_TYPE_DIESEL,
ENGINE_TYPE_GASOLINE,
)
Expand Down Expand Up @@ -345,46 +344,46 @@ async def test_in_progress(self):
async def test_is_primary_engine_electric(self):
"""Test primary electric engine."""
vehicle = Vehicle(conn=None, url="dummy34")
vehicle._states["StoredVehicleDataResponseParsed"] = {PRIMARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC}}
vehicle._states["StoredVehicleDataResponseParsed"] = {P.PRIMARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC}}
self.assertTrue(vehicle.is_primary_drive_electric())
self.assertFalse(vehicle.is_primary_drive_combustion())

async def test_is_primary_engine_combustion(self):
"""Test primary ICE."""
vehicle = Vehicle(conn=None, url="dummy34")
vehicle._states["StoredVehicleDataResponseParsed"] = {
PRIMARY_DRIVE: {"value": ENGINE_TYPE_DIESEL},
SECONDARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
P.PRIMARY_DRIVE: {"value": ENGINE_TYPE_DIESEL},
P.SECONDARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
}
self.assertTrue(vehicle.is_primary_drive_combustion())
self.assertFalse(vehicle.is_primary_drive_electric())
self.assertFalse(vehicle.is_secondary_drive_combustion())
self.assertTrue(vehicle.is_secondary_drive_electric())

# No secondary engine
vehicle._states["StoredVehicleDataResponseParsed"] = {PRIMARY_DRIVE: {"value": ENGINE_TYPE_GASOLINE}}
vehicle._states["StoredVehicleDataResponseParsed"] = {P.PRIMARY_DRIVE: {"value": ENGINE_TYPE_GASOLINE}}
self.assertTrue(vehicle.is_primary_drive_combustion())
self.assertFalse(vehicle.is_secondary_drive_electric())

async def test_has_combustion_engine(self):
"""Test check for ICE."""
vehicle = Vehicle(conn=None, url="dummy34")
vehicle._states["StoredVehicleDataResponseParsed"] = {
PRIMARY_DRIVE: {"value": ENGINE_TYPE_DIESEL},
SECONDARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
P.PRIMARY_DRIVE: {"value": ENGINE_TYPE_DIESEL},
P.SECONDARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
}
self.assertTrue(vehicle.has_combustion_engine())

# not sure if this exists, but :shrug:
vehicle._states["StoredVehicleDataResponseParsed"] = {
PRIMARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
SECONDARY_DRIVE: {"value": ENGINE_TYPE_GASOLINE},
P.PRIMARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
P.SECONDARY_DRIVE: {"value": ENGINE_TYPE_GASOLINE},
}
self.assertTrue(vehicle.has_combustion_engine())

# not sure if this exists, but :shrug:
vehicle._states["StoredVehicleDataResponseParsed"] = {
PRIMARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
SECONDARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
P.PRIMARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
P.SECONDARY_DRIVE: {"value": ENGINE_TYPE_ELECTRIC},
}
self.assertFalse(vehicle.has_combustion_engine())
29 changes: 16 additions & 13 deletions volkswagencarnet/vw_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@
import logging
import re
import secrets
import sys
import time
from base64 import b64encode, urlsafe_b64encode
from datetime import timedelta, datetime
from json import dumps as to_json
from random import random
from random import random, randint
from sys import version_info
from urllib.parse import urljoin, parse_qs, urlparse

import jwt
import sys
import time
from aiohttp import ClientSession, ClientTimeout, client_exceptions
from aiohttp.hdrs import METH_GET, METH_POST
from bs4 import BeautifulSoup
from pyrate_limiter import Duration, Limiter, RequestRate, BucketFullException
from sys import version_info

from volkswagencarnet.vw_exceptions import AuthenticationException
from volkswagencarnet.vw_timer import TimerData, TimersAndProfiles
Expand Down Expand Up @@ -59,10 +59,8 @@ class Connection:

ALLOW_RATE_LIMIT_DELAY = True

# 5 / 10 seconds, 12 / minute, 30 / 5 minutes
limiter = Limiter(
RequestRate(3, Duration.SECOND * 10), RequestRate(12, Duration.MINUTE), RequestRate(30, Duration.MINUTE * 5)
)
# 20 / minute, 50 / 5 minutes
limiter = Limiter(RequestRate(20, Duration.MINUTE), RequestRate(50, Duration.MINUTE * 5))

# Init connection class
def __init__(self, session, username, password, fulldebug=False, country=COUNTRY, interval=timedelta(minutes=5)):
Expand Down Expand Up @@ -510,7 +508,7 @@ async def _request(self, method, url, **kwargs):
_LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}]')
return res

async def get(self, url, vin=""):
async def get(self, url, vin="", tries=0):
"""Perform a get query."""
try:
response = await self._request(METH_GET, self._make_url(url, vin))
Expand All @@ -519,14 +517,19 @@ async def get(self, url, vin=""):
_LOGGER.error(f"Bucket full: Refusing to send more requests to backend. {ex}")
return {"status_code": 429, "status_message": "Own rate limit exceeded."}
except client_exceptions.ClientResponseError as error:
if error.status == 401:
_LOGGER.warning(f'Received "unauthorized" error while fetching data: {error}')
self._session_logged_in = False
elif error.status == 400:
if error.status == 400:
_LOGGER.error(
'Got HTTP 400 "Bad Request" from server, this request might be malformed or not implemented'
" correctly for this vehicle"
)
elif error.status == 401:
_LOGGER.warning(f'Received "unauthorized" error while fetching data: {error}')
self._session_logged_in = False
elif error.status == 429 and tries < 3:
delay = randint(1, 3 + tries * 2)
_LOGGER.debug(f"Server side throttled. Waiting {delay}, try {tries + 1}")
await asyncio.sleep(delay)
return await self.get(url, vin, tries + 1)
elif error.status == 500:
_LOGGER.info("Got HTTP 500 from server, service might be temporarily unavailable")
elif error.status == 502:
Expand Down
22 changes: 22 additions & 0 deletions volkswagencarnet/vw_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,25 @@ class VWDeviceClass:
POWER = "power"
TEMPERATURE = "temperature"
WINDOW = "window"


class VehicleStatusParameter:
"""Hex codes for vehicle status parameters."""

FRONT_LEFT_DOOR_LOCK = "0x0301040001"
FRONT_RIGHT_DOOR_LOCK = "0x0301040007"
REAR_LEFT_DOOR_LOCK = "0x0301040004"
READ_RIGHT_DOOR_LOCK = "0x030104000A"

HOOD_CLOSED = "0x0301040011"

TRUNK_LOCK = "0x030104000D"

PRIMARY_RANGE = "0x0301030006"
SECONDARY_RANGE = "0x0301030008"

PRIMARY_DRIVE = "0x0301030007"
SECONDARY_DRIVE = "0x0301030009"
COMBINED_RANGE = "0x0301030005"

FUEL_LEVEL = "0x030103000A"
Loading

0 comments on commit 1109b22

Please sign in to comment.