Skip to content

Commit

Permalink
Add Jena
Browse files Browse the repository at this point in the history
  • Loading branch information
Mola19 committed Jan 2, 2024
1 parent f044e8b commit 1f3a831
Show file tree
Hide file tree
Showing 4 changed files with 364 additions and 0 deletions.
27 changes: 27 additions & 0 deletions park_api/cities/Jena.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
50.9282,
11.5880
]
},
"properties": {
"name": "Jena",
"type": "city",
"url": "https://mobilitaet.jena.de/",
"source": "https://opendata.jena.de/data/parkplatzbelegung.xml",
"active_support": false,
"attribution": {
"contributor":"Kommunal Service Jena",
"url":"https://opendata.jena.de/dataset/parken",
"license":"dl-de/by-2-0"
}
}
}
]
}
207 changes: 207 additions & 0 deletions park_api/cities/Jena.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import json
import requests
import pytz
from datetime import datetime, time
from bs4 import BeautifulSoup
from park_api import env
from park_api.geodata import GeoData
from park_api.util import convert_date

# there is no need for any geodata for this file, as the api returns all of the information,
# but if this is removed, the code crashes
geodata = GeoData(__file__)

def parse_html(lot_vacancy_xml):

# there is a second source with all the general data for the parking lots
HEADERS = {
"User-Agent": "ParkAPI v%s - Info: %s" %
(env.SERVER_VERSION, env.SOURCE_REPOSITORY),
}

lot_data_json = requests.get("https://opendata.jena.de/dataset/1a542cd2-c424-4fb6-b30b-d7be84b701c8/resource/76b93cff-4f6c-47fa-ab83-b07d64c8f38a/download/parking.json", headers={**HEADERS})

lot_vacancy = BeautifulSoup(lot_vacancy_xml, "xml")
lot_data = json.loads(lot_data_json.text)

data = {
# the time contains the timezone and milliseconds which need to be stripped
"last_updated": lot_vacancy.find("publicationTime").text.split(".")[0],
"lots": []
}

for lot in lot_data["parkingPlaces"]:
# the lots from both sources need to be matched
lot_data_list = [
_lot for _lot in lot_vacancy.find_all("parkingFacilityStatus")
if hasattr(_lot.parkingFacilityReference, "attr")
and _lot.parkingFacilityReference.attrs["id"] == lot["general"]["name"]
]

lot_id = lot["general"]["name"].lower().replace(" ", "-").replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")

lot_info = {
"id": lot_id,
"name": lot["general"]["name"],
"url": "https://mobilitaet.jena.de/de/" + lot_id,
"address": lot["details"]["parkingPlaceAddress"]["parkingPlaceAddress"],
"coords": lot["general"]["coordinates"],
"state": get_status(lot),
"lot_type": lot["general"]["objectType"],
"opening_hours": parse_opening_hours(lot),
"fee_hours": parse_charged_hours(lot),
"forecast": False,
}

# some lots do not have live vacancy data
if len(lot_data_list) > 0:
lot_info["free"] = int(lot_data_list[0].totalNumberOfVacantParkingSpaces.text)
lot_info["total"] = int(lot_data_list[0].totalParkingCapacityShortTermOverride.text)
else:
continue
# lot_info["free"] = None
# lot_info["total"] = int(lot["details"]["parkingCapacity"]["totalParkingCapacityShortTermOverride"])
# note: both api's have different values for the total parking capacity,
# but the vacant slot are based on the total parking capacity from the same api,
# so that is used if available

# also in the vacancy api the total capacity for the "Goethe Gallerie" are 0 if it is closed


data["lots"].append(lot_info)

return data

# the rest of the code is there to deal with the api's opening/charging hours objects
# example:
# "openingTimes": [
# {
# "alwaysCharged": True,
# "dateFrom": 2,
# "dateTo": 5,
# "times": [
# {
# "from": "07:00",
# "to": "23:00"
# }
# ]
# },
# {
# "alwaysCharged": False,
# "dateFrom": 7,
# "dateTo": 1,
# "times": [
# {
# "from": "10:00",
# "to": "03:00"
# }
# ]
# }
# ]

def parse_opening_hours(lot_data):
if lot_data["parkingTime"]["openTwentyFourSeven"]: return "24/7"

return parse_times(lot_data["parkingTime"]["openingTimes"])

def parse_charged_hours(lot_data):
charged_hour_objs = []

ph_info = "An Feiertagen sowie außerhalb der oben genannten Zeiten ist das Parken gebührenfrei."

if not lot_data["parkingTime"]["chargedOpeningTimes"] and lot_data["parkingTime"]["openTwentyFourSeven"]:
if lot_data["priceList"]:
if ph_info in str(lot_data["priceList"]["priceInfo"]):
return "24/7; PH off"
else: return "24/7"
else: return "off"

# charging hours can also be indicated by the "alwaysCharged" variable in "openingTimes"
elif not lot_data["parkingTime"]["chargedOpeningTimes"] and not lot_data["parkingTime"]["openTwentyFourSeven"]:
for oh in lot_data["parkingTime"]["openingTimes"]:
if "alwaysCharged" in oh and oh["alwaysCharged"]: charged_hour_objs.append(oh)
if len(charged_hour_objs) == 0: return "off"

elif lot_data["parkingTime"]["chargedOpeningTimes"]:
charged_hour_objs = lot_data["parkingTime"]["chargedOpeningTimes"]

charged_hours = parse_times(charged_hour_objs)

if ph_info in str(lot_data["priceList"]["priceInfo"]):
charged_hours += "; PH off"

return charged_hours

# creatin osm opening_hours strings from opening/charging hours objects
def parse_times(times_objs):
DAYS = ["", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]

ohs = ""

for index, oh in enumerate(times_objs):
part = ""

if oh["dateFrom"] == oh["dateTo"]:
part += DAYS[oh["dateFrom"]]
else:
part += DAYS[oh["dateFrom"]] + "-" + DAYS[oh["dateTo"]]

part += " "

for index2, time in enumerate(oh["times"]):
part += time["from"] + "-" + time["to"]
if index2 != len(oh["times"]) - 1: part += ","

if index != len(times_objs) - 1: part += "; "

ohs += part

return ohs

def get_status(lot_data):
if lot_data["parkingTime"]["openTwentyFourSeven"]: return "open"

# check for public holiday?

for oh in lot_data["parkingTime"]["openingTimes"]:
now = datetime.now(pytz.timezone("Europe/Berlin"))

weekday = now.weekday() + 1

# oh rules can also go beyond week ends (e.g. from Sunday to Monday)
# this need to be treated differently
if oh["dateFrom"] <= oh["dateTo"]:
if not (weekday >= oh["dateFrom"]) or not (weekday <= oh["dateTo"] + 1): continue
else:
if weekday > oh["dateTo"] + 1 and weekday < oh["dateFrom"]: continue

for times in oh["times"]:
time_from = get_timestamp_without_date(time.fromisoformat(times["from"]).replace(tzinfo=pytz.timezone("Europe/Berlin")))
time_to = get_timestamp_without_date(time.fromisoformat(times["to"]).replace(tzinfo=pytz.timezone("Europe/Berlin")))

time_now = get_timestamp_without_date(now)

# time ranges can go over to the next day (e.g 10:00-03:00)
if time_to >= time_from:
if time_now >= time_from and time_now <= time_to:
return "open"
else: continue

else:
if oh["dateFrom"] <= oh["dateTo"]:
if (time_now >= time_from and weekday >= oh["dateFrom"] and weekday <= oh["dateTo"]
or time_now <= time_to and weekday >= oh["dateFrom"] + 1 and weekday <= oh["dateTo"] + 1):
return "open"
else: continue

else:
if (time_now >= time_from and (weekday >= oh["dateFrom"] or weekday <= oh["dateTo"])
or time_now <= time_to and (weekday >= oh["dateFrom"] + 1 or weekday <= oh["dateTo"] + 1)):
return "open"
else: continue

# if no matching rule was found, the lot is closed
return "closed"

def get_timestamp_without_date (date_obj):
return date_obj.hour * 3600 + date_obj.minute * 60 + date_obj.second
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ yoyo-migrations
requests-mock
utm
ddt
lxml
129 changes: 129 additions & 0 deletions tests/fixtures/jena.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<d2LogicalModel xmlns="http://datex2.eu/schema/2/2_0">
<payloadPublication xsi:type="GenericPublication" lang="de" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<publicationTime>2023-12-31T16:10:47.378+01:00</publicationTime>
<publicationCreator>
<country>de</country>
<nationalIdentifier>Kommunal Service Jena</nationalIdentifier>
</publicationCreator>
<genericPublicationExtension>
<parkingFacilityTableStatusPublication>
<headerInformation>
<confidentiality>noRestriction</confidentiality>
<informationStatus>real</informationStatus>
</headerInformation>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>0.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="City Carree" version="SYSTEM"/>
<parkingFacilityStatusTime>2023-12-30T23:10:39.826+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>0</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>0</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>0</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>12.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="Seidelparkplatz" version="SYSTEM"/>
<parkingFacilityStatusTime>2023-12-31T16:05:45.504+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>20</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>141</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>161</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>8.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="Krautgasse" version="SYSTEM"/>
<parkingFacilityStatusTime>2023-12-31T14:11:34.771+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>15</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>178</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>193</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>0.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="Goethe Galerie" version="SYSTEM"/>
<parkingFacilityStatusTime>2023-12-30T23:00:29.090+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>0</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>0</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>0</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>5.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="Neue Mitte" version="SYSTEM"/>
<parkingFacilityStatusTime>2023-12-31T16:07:44.769+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>10</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>180</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>190</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>19.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="Windberg" version="SYSTEM"/>
<parkingFacilityStatusTime>2023-12-31T15:32:41.436+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>14</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>61</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>75</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>0.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="Eichplatz" version="SYSTEM"/>
<parkingFacilityStatusTime>2023-11-16T07:26:33.218+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>0</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>0</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>0</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>100.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="Haeckelplatz" version="SYSTEM"/>
<parkingFacilityStatus>full</parkingFacilityStatus>
<parkingFacilityStatusTime>2023-12-28T13:11:30.623+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>32</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>0</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>32</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>29.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="Holzmarkt" version="SYSTEM"/>
<parkingFacilityStatusTime>2023-12-31T16:08:44.571+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>44</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>106</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>150</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
<parkingFacilityStatus>
<parkingFacilityExitRate>0</parkingFacilityExitRate>
<parkingFacilityFillRate>0</parkingFacilityFillRate>
<parkingFacilityOccupancy>43.0</parkingFacilityOccupancy>
<parkingFacilityOccupancyTrend>decreasing</parkingFacilityOccupancyTrend>
<parkingFacilityReference id="Steinkreuz" version="SYSTEM"/>
<parkingFacilityStatusTime>2023-12-31T16:08:44.586+01:00</parkingFacilityStatusTime>
<totalNumberOfOccupiedParkingSpaces>26</totalNumberOfOccupiedParkingSpaces>
<totalNumberOfVacantParkingSpaces>34</totalNumberOfVacantParkingSpaces>
<totalParkingCapacityShortTermOverride>60</totalParkingCapacityShortTermOverride>
</parkingFacilityStatus>
</parkingFacilityTableStatusPublication>
</genericPublicationExtension>
</payloadPublication>
</d2LogicalModel>

0 comments on commit 1f3a831

Please sign in to comment.