Skip to content

Commit

Permalink
Add Stadtmobil Stuttgart (#72)
Browse files Browse the repository at this point in the history
and additionally:

* read available pricing plans from config instead of subclasses
* remove currently unnecessary CantamenIXSIProvider subclasses
* cast CANTAMEN_IXSI_API_TIMEOUT to int

---------

Co-authored-by: Thorsten Fröhlinghaus <[email protected]>
  • Loading branch information
hbruch and ThorstenFroehlinghaus authored Feb 21, 2024
1 parent 3a8f227 commit 6a6f79c
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 27 deletions.
242 changes: 242 additions & 0 deletions config/stadtmobil_stuttgart.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
{
"feed_data": {
"pricing_plans": [
{
"currency": "EUR",
"description": "Classik-Tarif: 1,40€/h. Nachtpreis zwischen 0h und 7h: 0€. 0,25€/km bis 100km, danach 0,23€/km. Zuzüglich Grundgebühr. Alle Angaben ohne Gewähr.",
"is_taxable": false,
"name": "A Toyota Aygo",
"per_min_pricing": [
{
"start": 0,
"end": 1020,
"interval": 60,
"rate": 1.40
},
{
"start": 1020,
"end": 1440,
"interval": 60,
"rate": 0
}
],
"per_km_pricing": [
{
"start": 0,
"end": 100,
"interval": 1,
"rate": 0.25
},
{
"start": 100,
"interval": 1,
"rate": 0.23
}
],
"plan_id": "mini",
"price": 0,
"url": "https://stuttgart.stadtmobil.de/media/user_upload/downloads_privatkunden/stuttgart/Tarifordnung_05_2022.pdf"
},
{
"currency": "EUR",
"description": "Classik-Tarif: 2,20€/h. Nachtpreis zwischen 0h und 7h: 0€. 0,27€/km bis 100km, danach 0,24€/km bis 700km, danach 0,21€/km. Zuzüglich Grundgebühr. Alle Angaben ohne Gewähr.",
"is_taxable": false,
"name": "B Opel Corsa, Opel Corsa (Elektro), Opel Adam, Renault ZOE (Elektro), Toyota Yaris Hybrid, Opel Cargo Kastenwagen, Renault Kangoo Kastenwagen",
"per_min_pricing": [
{
"start": 0,
"end": 1020,
"interval": 60,
"rate": 2.20
},
{
"start": 1020,
"end": 1440,
"interval": 60,
"rate": 0
}
],
"per_km_pricing": [
{
"start": 0,
"end": 100,
"interval": 1,
"rate": 0.27
},
{
"start": 100,
"end": 700,
"interval": 1,
"rate": 0.24
},
{
"start": 700,
"interval": 1,
"rate": 0.21
}
],
"plan_id": "small",
"price": 0,
"url": "https://stuttgart.stadtmobil.de/media/user_upload/downloads_privatkunden/stuttgart/Tarifordnung_05_2022.pdf"
},
{
"currency": "EUR",
"description": "Classik-Tarif: 2,80€/h. Nachtpreis zwischen 0h und 7h: 0€. 0,31€/km bis 100km, danach 0,26€/km bis 700km, danach 0,22€/km. Zuzüglich Grundgebühr. Alle Angaben ohne Gewähr.",
"is_taxable": false,
"name": "C Opel Astra Kombi, Toyota Auris/Corolla Kombi Hybrid, Opel Life Hochdachkombi",
"per_min_pricing": [
{
"start": 0,
"end": 1020,
"interval": 60,
"rate": 2.80
},
{
"start": 1020,
"end": 1440,
"interval": 60,
"rate": 0
}
],
"per_km_pricing": [
{
"start": 0,
"end": 100,
"interval": 1,
"rate": 0.31
},
{
"start": 100,
"end": 700,
"interval": 1,
"rate": 0.26
},
{
"start": 700,
"interval": 1,
"rate": 0.22
}
],
"plan_id": "medium",
"price": 0,
"url": "https://stuttgart.stadtmobil.de/media/user_upload/downloads_privatkunden/stuttgart/Tarifordnung_05_2022.pdf"
},
{
"currency": "EUR",
"description": "Classik-Tarif: 3,20€/h. Nachtpreis zwischen 0h und 7h: 1,00€. 0,34€/km bis 100km, danach 0,30€/km. Zuzüglich Grundgebühr. Alle Angaben ohne Gewähr.",
"is_taxable": false,
"name": "D BMW 216d Active Tourer Opel Zafira Kleinbus, Ford Custom Kleinbus, Renault Trafic (8-Sitzer), Renault Trafic (9-Sitzer",
"per_min_pricing": [
{
"start": 0,
"end": 1020,
"interval": 60,
"rate": 3.20
},
{
"start": 1020,
"end": 1440,
"interval": 60,
"rate": 1.00
}
],
"per_km_pricing": [
{
"start": 0,
"end": 100,
"interval": 1,
"rate": 0.34
},
{
"start": 100,
"interval": 1,
"rate": 0.30
}
],
"plan_id": "van",
"price": 0,
"url": "https://stuttgart.stadtmobil.de/media/user_upload/downloads_privatkunden/stuttgart/Tarifordnung_05_2022.pdf"
},
{
"currency": "EUR",
"description": "Classik-Tarif: 4,20€/h. Nachtpreis zwischen 0h und 7h: 2,00€. 0,38€/km bis 100km, danach 0,32€/km. Zuzüglich Grundgebühr. Alle Angaben ohne Gewähr.",
"is_taxable": false,
"name": "F Ford Transit Transporter",
"per_min_pricing": [
{
"start": 0,
"end": 1020,
"interval": 60,
"rate": 4.20
},
{
"start": 1020,
"end": 1440,
"interval": 60,
"rate": 2.00
}
],
"per_km_pricing": [
{
"start": 0,
"end": 100,
"interval": 1,
"rate": 0.38
},
{
"start": 100,
"interval": 1,
"rate": 0.32
}
],
"plan_id": "transporter",
"price": 0,
"url": "https://stuttgart.stadtmobil.de/media/user_upload/downloads_privatkunden/stuttgart/Tarifordnung_05_2022.pdf"
}
],
"system_information": {
"email": "[email protected]",
"feed_contact_email": "[email protected]",
"language": "de",
"license_url": "https://www.govdata.de/dl-de/by-2-0",
"license_id": "DL-DE-BY-2.0",
"attribution_organization_name": "stadtmobil carsharing AG",
"attribution_url": "https://stuttgart.stadtmobil.de/",
"name": "Stadtmobil Stuttgart",
"operator": "stadtmobil carsharing AG",
"phone_number": "+49 (0)711 - 94 54 36 36",
"privacy_url": "https://stuttgart.stadtmobil.de/datenschutz/",
"purchase_url": "https://stuttgart.stadtmobil.de/privatkunden/kunde-werden/",
"rental_apps": {
"android": {
"discovery_uri": "cantamen-interapp-csd://add_booking",
"store_uri": "https://play.google.com/store/apps/details?id=de.cantamen.carsharing.deutschland"
},
"ios": {
"discovery_uri": "cantamen-interapp-csd://add_booking",
"store_uri": "https://apps.apple.com/de/app/carsharing-deutschland/id1075669223"
}
},
"system_id": "stadtmobil_stuttgart",
"terms_last_updated": "2020-02-01",
"terms_url": "https://stuttgart.stadtmobil.de/agb/",
"timezone": "Europe/Berlin",
"url": "https://stuttgart.stadtmobil.de/",
"_feed_notice": "Keine Echtzeitdaten zum Buchungsstatus der Fahrzeuge. Verfügbarkeit ist im Buchungssystem des Anbieters sichtbar."
}
},
"system_id": "10024",
"provider_id": 3,
"url_templates": {
"station": {
"android": "cantamen-interapp-csd://add_booking?openPlace={placeId}",
"ios": "cantamen-interapp-csd://add_booking?openPlace={placeId}",
"web": "https://ewi3.cantamen.de/?openPlace={placeId}"
},
"vehicle": {
"android": "cantamen-interapp-csd://add_booking?openBookee={bookeeId}",
"ios": "cantamen-interapp-csd://add_booking?openBookee={bookeeId}",
"web": "https://ewi3.cantamen.de/?openBookee={bookeeId}"
}
}
}
2 changes: 1 addition & 1 deletion x2gbfs/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .cantamen import MyECarProvider, StadtmobilSuedbadenProvider
from .cantamen import CantamenIXSIProvider
from .deer import Deer
from .example import ExampleProvider
from .fleetster import FleetsterAPI
Expand Down
47 changes: 27 additions & 20 deletions x2gbfs/providers/cantamen.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class CantamenIXSIProvider(BaseProvider):
'childsafetyseat15to36': 'child_seat_c',
# propulsion_type mappings (hybrid/electric are already GBFS conformant)
'diesel': 'combustion_diesel',
'gasoline': 'combustion',
'dieselfromeuro6': 'combustion_diesel',
}

# attributes that map to GBFS `vehicle_accessories` entries
Expand All @@ -72,21 +74,22 @@ class CantamenIXSIProvider(BaseProvider):
# attributes that map to GBFS `vehicle_equipment` entries
EQUIPMENT_ATTRIBUTES = ['winter_tires', 'child_seat_c']
# attributes that map to GBFS `propulsion_type` (naturalgas is no GBFS ppropulsion type yet)
PROPULSION_ATTRIBUTES = ['hybrid', 'combustion_diesel', 'electric']
# Currently supported pricing plan IDs (These need to be configured in config/<provider>.json)
PRICING_PLAN_IDS = ['stationwagon', 'micro', 'mini', 'small', 'large', 'transporter']
PROPULSION_ATTRIBUTES = ['hybrid', 'combustion', 'combustion_diesel', 'electric']

cached_response: Optional[Dict[str, Any]] = None
attributes: Dict[str, str] = {}
# maps IXSI color attributes' IDs to their respective color names, e.g. "10648" (with Code COL_RED) -> "Rot"
colors: Dict[str, str] = {}
seats: Dict[str, int] = {}
# Currently supported pricing plan IDs (These need to be configured in config/<provider>.json)
pricing_plan_ids: List[str] = []

def __init__(self, feed_config):
self.api_url = config('CANTAMEN_IXSI_API_URL')
self.api_timeout = config('CANTAMEN_IXSI_API_TIMEOUT', 10)
self.api_timeout = int(config('CANTAMEN_IXSI_API_TIMEOUT', 10))
self.api_response_max_size = config('CANTAMEN_IXSI_RESPONSE_MAX_SIZE', 2**24)
self.config = feed_config
self.pricing_plan_ids = [plan['plan_id'] for plan in feed_config['feed_data']['pricing_plans']]

def _load_response(self) -> Dict[str, Any]:
if not self.cached_response:
Expand Down Expand Up @@ -121,6 +124,9 @@ def _parse_attributes(self, attributes):
def _all_bookees(self) -> Generator[Dict[str, Any], None, None]:
bookees = self._load_response()['Bookee']
for bookee in bookees:
if bookee.get('Class') is None:
logger.info(f'Bookee {bookee["ID"]} has no Class and will be ignored')
continue
yield bookee

def _all_places(self) -> Generator[Dict[str, Any], None, None]:
Expand Down Expand Up @@ -181,26 +187,35 @@ def _extract_propulsion_type_and_range(

# first propulsion_type attribute ot None, if None declared for this bookee
propulsion_type = next(iter(self._filter_and_map_attributes(attributes, self.PROPULSION_ATTRIBUTES)), None)
is_hybrid = next(iter(self._filter_and_map_attributes(attributes, ['hybrid'])), None)

if form_factor == 'cargo_bicycle':
if is_hybrid == 'hybrid':
propulsion_type = 'hybrid'
elif form_factor == 'cargo_bicycle':
propulsion_type = 'electric_assist'
max_range_meters = self.DEFAULT_CARGO_BIKE_MAX_RANGE_METERS
elif propulsion_type == 'electric' or 'CurrentStateOfCharge' in bookee or 'km' in name:
elif propulsion_type == 'electric' or (
propulsion_type is None and ('CurrentStateOfCharge' in bookee or 'km' in name)
):
propulsion_type = 'electric'
pattern = re.compile(r'.*\< ?(\d*) ?km')
match = pattern.match(name)
if match:
max_range_meters = int(match.group(1)) * 1000
else:
elif propulsion_type is None:
propulsion_type = 'combustion' # if no combustion type can be derived, we assume combustion

return propulsion_type, max_range_meters

def _extract_pricing_plan_id(self, bookee: Dict, attributes: List[str]) -> str:
# if this bookee is a stationwagen, pricing_plan_id `stationwagen` is returned.
# Otherwise, the bookee's lowercased `Class`. This needs to be a supported pricing_plan_id
pricing_plan_id = 'stationwagon' if self._is_stationwagon(attributes) else bookee['Class'].lower()
if pricing_plan_id not in self.PRICING_PLAN_IDS:
pricing_plan_id = (
'stationwagon'
if self._is_stationwagon(attributes) and 'stationwagon' in self.pricing_plan_ids
else bookee['Class'].lower()
)
if pricing_plan_id not in self.pricing_plan_ids:
raise ValueError('Unexpected bookee class {} is no pricing_plan_id'.format(pricing_plan_id))

return pricing_plan_id
Expand All @@ -215,7 +230,9 @@ def _extract_vehicle_name(self, bookee_name: str) -> str:
# Vehicles usually have their license plate (in parentheses) appended in their name.
# We cut this of by cutting of all text starting from the rightmost opening parenthesis.
# A single license plate (X-XXX XXX (BÜ)) is handled explicitly here
name = bookee_name[0 : bookee_name.replace('(BÜ)', '').rfind('(')].strip()
name = bookee_name.replace('(BÜ)', '')
name = re.sub(r'\(\d+\)', '', name)
name = name[0 : name.rfind('(')].strip() if name.rfind('(') > 0 else name.strip()
# range is spelled in various ways (e.g. 'bis XXXkm', '< XXXkm', '<XXXkm'), we homogenize ot '<XXXkm'':
return name.replace(' bis ', ' <').replace('< ', '<')

Expand Down Expand Up @@ -338,13 +355,3 @@ def load_stations_and_vehicles(
self._add_rental_uris(station_infos_map, vehicles_map)

return station_infos_map, station_status_map, vehicle_types_map, vehicles_map


class MyECarProvider(CantamenIXSIProvider):
# Currently supported pricing plan IDs (These need to be configured in config/<provider>.json)
PRICING_PLAN_IDS = ['stationwagon', 'micro', 'mini', 'small', 'large', 'transporter', 'bike']


class StadtmobilSuedbadenProvider(CantamenIXSIProvider):
# Currently supported pricing plan IDs (These need to be configured in config/<provider>.json)
PRICING_PLAN_IDS = ['stationwagon', 'micro', 'mini', 'small', 'large', 'transporter']
9 changes: 3 additions & 6 deletions x2gbfs/x2gbfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@

from x2gbfs.gbfs import BaseProvider, GbfsTransformer, GbfsWriter
from x2gbfs.providers import (
CantamenIXSIProvider,
Deer,
ExampleProvider,
FleetsterAPI,
LastenVeloFreiburgProvider,
MyECarProvider,
StadtmobilSuedbadenProvider,
VoiRaumobil,
)

Expand All @@ -43,10 +42,8 @@ def build_extractor(provider: str, feed_config: Dict[str, Any]) -> BaseProvider:
api_password = config('VOI_PASSWORD')

return VoiRaumobil(api_url, api_user, api_password)
if provider in ['stadtmobil_suedbaden']:
return StadtmobilSuedbadenProvider(feed_config)
if provider in ['my-e-car']:
return MyECarProvider(feed_config)
if provider in ['my-e-car'] or provider.startswith('stadtmobil_'):
return CantamenIXSIProvider(feed_config)

raise ValueError(f'Unknown config {provider}')

Expand Down

0 comments on commit 6a6f79c

Please sign in to comment.