We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[Source Request]: 
### Municipality / Region

Bedburg

### Collection Calendar Webpage

https://buerger-portal-bedburg.azurewebsites.net/calendar/

### Example Address

Am Schirkerhof 2, Bedburg, Broich

### Collection Data Format

Something different (add to additional information)

### Additional Information

The city of Bedburg change its Waste Collection Provider. Thus the old Abfall+ Source doesn't work anymore. The new provider is Drekopf. Unfortunately they do not use the existing Drekopf API. The Webpage provided by Drekopf has multiple outputs. You can download the calendar via PDF, XLS, CSV and ICS, but i can't find the url. The underlying API seems to be C-Trace, but when calling this API, credentials are requested. Maybe i am missing something...

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py`
Content:
```
1 import datetime
2 import re
3 from dataclasses import dataclass
4 from typing import List, Literal, Optional, TypedDict, Union
5 
6 import requests
7 from waste_collection_schedule import Collection  # type: ignore[attr-defined]
8 
9 TITLE = "Bürgerportal"
10 URL = "https://www.c-trace.de"
11 DESCRIPTION = "Source for waste collection in multiple service areas."
12 
13 
14 def EXTRA_INFO():
15     return [{"title": s["title"], "url": s["url"]} for s in SERVICE_MAP]
16 
17 
18 TEST_CASES = {
19     "Cochem-Zell": {
20         "operator": "cochem_zell",
21         "district": "Bullay",
22         "subdistrict": "Bullay",
23         "street": "Layenweg",
24         "number": 3,
25     },
26     "Alb-Donau": {
27         "operator": "alb_donau",
28         "district": "Blaubeuren",
29         "street": "Alberstraße",
30         "number": 3,
31     },
32     "Biedenkopf": {
33         "operator": "biedenkopf",
34         "district": "Biedenkopf",
35         "subdistrict": "Breidenstein",
36         "street": "Auf dem Hammer",
37         "number": 1,
38     },
39 }
40 
41 ICON_MAP = {
42     "mobil": "mdi:truck",
43     "bio": "mdi:leaf",
44     "papier": "mdi:package-variant",
45     "verpackung": "mdi:recycle",
46     "gelb": "mdi:recycle",
47     "lvp": "mdi:recycle",
48     "rest": "mdi:trash-can",
49     "gruen": "mdi:forest",
50     "grün": "mdi:forest",
51     "baum": "mdi:forest",
52     "schnitt": "mdi:forest",
53     "schad": "mdi:biohazard",
54 }
55 API_HEADERS = {
56     "Accept": "application/json, text/plain;q=0.5",
57     "Cache-Control": "no-cache",
58 }
59 Operator = Literal["cochem_zell", "alb_donau", "biedenkopf"]
60 
61 SERVICE_MAP = [
62     {
63         "title": "KV Cochem-Zell",
64         "url": "https://www.cochem-zell-online.de/",
65         "api_url": "https://buerger-portal-cochemzell.azurewebsites.net/api",
66         "operator": "cochem_zell",
67     },
68     {
69         "title": "Abfallwirtschaft Alb-Donau-Kreis",
70         "url": "https://www.aw-adk.de/",
71         "api_url": "https://buerger-portal-albdonaukreisabfallwirtschaft.azurewebsites.net/api",
72         "operator": "alb_donau",
73     },
74     {
75         "title": "MZV Biedenkopf",
76         "url": "https://mzv-biedenkopf.de/",
77         "api_url": "https://biedenkopfmzv.buergerportal.digital/api",
78         "operator": "biedenkopf",
79     },
80 ]
81 
82 
83 # This datalcass is used for adding entries to a set and remove duplicate entries.
84 # The default `Collection` extends the standard dict and thus is not hashable.
85 @dataclass(frozen=True, eq=True)
86 class CollectionEntry:
87     date: datetime.date
88     waste_type: str
89     icon: Optional[str]
90 
91     def export(self) -> Collection:
92         return Collection(self.date, self.waste_type, self.icon)
93 
94 
95 def quote_none(value: Optional[str]) -> str:
96     if value is None:
97         return "null"
98 
99     return f"'{value}'"
100 
101 
102 def get_api_map():
103     return {s["operator"]: s["api_url"] for s in SERVICE_MAP}
104 
105 
106 class Source:
107     def __init__(
108         self,
109         operator: Operator,
110         district: str,
111         street: str,
112         subdistrict: Optional[str] = None,
113         number: Union[int, str, None] = None,
114         show_volume: bool = False,
115     ):
116         self.api_url = get_api_map()[operator]
117         self.district = district
118         self.subdistrict = subdistrict
119         self.street = street
120         self.number = number
121         self.show_volume = show_volume
122 
123     def fetch(self) -> list[Collection]:
124         session = requests.session()
125         session.headers.update(API_HEADERS)
126 
127         year = datetime.datetime.now().year
128         entries: set[CollectionEntry] = set()
129 
130         district_id = self.fetch_district_id(session)
131         street_id = self.fetch_street_id(session, district_id)
132         # Eventually verify house number in the future
133 
134         params = {
135             "$expand": "Abfuhrplan,Abfuhrplan/GefaesstarifArt/Abfallart,Abfuhrplan/GefaesstarifArt/Volumen",
136             "$orderby": "Abfuhrplan/GefaesstarifArt/Abfallart/Name,Abfuhrplan/GefaesstarifArt/Volumen/VolumenWert",
137             "orteId": district_id,
138             "strassenId": street_id,
139             "jahr": year,
140         }
141 
142         if self.number:
143             params["hausNr"] = (f"'{self.number}'",)
144 
145         res = session.get(
146             f"{self.api_url}/AbfuhrtermineAbJahr",
147             params=params,
148         )
149         res.raise_for_status()
150         payload: CollectionsRes = res.json()
151 
152         date_regex = re.compile(r"\d+")
153 
154         for collection in payload["d"]:
155             if date_match := re.search(date_regex, collection["Termin"]):
156                 timestamp = float(date_match.group())
157                 date = datetime.datetime.utcfromtimestamp(timestamp / 1000).date()
158                 waste_type = collection["Abfuhrplan"]["GefaesstarifArt"]["Abfallart"][
159                     "Name"
160                 ]
161                 icon = None
162 
163                 for icon_type, tested_icon in ICON_MAP.items():
164                     if icon_type.lower() in waste_type.lower():
165                         icon = tested_icon
166 
167                 if self.show_volume:
168                     volume = int(
169                         collection["Abfuhrplan"]["GefaesstarifArt"]["Volumen"][
170                             "VolumenWert"
171                         ]
172                     )
173                     waste_type = f"{waste_type} ({volume} l)"
174 
175                 entries.add(CollectionEntry(date, waste_type, icon))
176 
177         if len(entries) == 0:
178             raise ValueError(
179                 "No collections found! Please verify that your configuration is correct."
180             )
181 
182         return [entry.export() for entry in entries]
183 
184     def fetch_district_id(self, session: requests.Session) -> int:
185         res = session.get(
186             f"{self.api_url}/OrteMitOrtsteilen",
187             headers=API_HEADERS,
188         )
189         res.raise_for_status()
190         payload: DistrictsRes = res.json()
191 
192         try:
193             return next(
194                 entry["OrteId"]
195                 for entry in payload["d"]
196                 if entry["Ortsname"] == self.district
197                 and entry["Ortsteilname"] == self.subdistrict
198             )
199         except StopIteration:
200             raise ValueError(
201                 "District id cannot be fetched. "
202                 "Please make sure that you entered a subdistrict if there is a comma on the website."
203             )
204 
205     def fetch_street_id(self, session: requests.Session, district_id: int):
206         res = session.get(
207             f"{self.api_url}/Strassen",
208             params={
209                 "$filter": f"Ort/OrteId eq {district_id} and OrtsteilName eq {quote_none(self.subdistrict)}",
210                 "$orderby": "Name asc",
211             },
212             headers=API_HEADERS,
213         )
214         res.raise_for_status()
215         payload: StreetsRes = res.json()
216 
217         try:
218             return next(
219                 entry["StrassenId"]
220                 for entry in payload["d"]
221                 if entry["Name"] == self.street
222             )
223         except StopIteration:
224             raise ValueError(
225                 "Street ID cannot be fetched. Please verify your configuration."
226             )
227 
228 
229 # Typed dictionaries for the API
230 # Automatically generated using https://pytyper.dev/
231 
232 
233 class DistrictRes(TypedDict):
234     OrteId: int
235     Ortsname: str
236     Ortsteilname: Optional[str]
237 
238 
239 class DistrictsRes(TypedDict):
240     d: List[DistrictRes]
241 
242 
243 class StreetRes(TypedDict):
244     StrassenId: int
245     Name: str
246     Plz: str
247 
248 
249 class StreetsRes(TypedDict):
250     d: List[StreetRes]
251 
252 
253 class Capacity(TypedDict):
254     VolumenId: int
255     VolumenWert: str
256 
257 
258 class WasteType(TypedDict):
259     AbfallartenId: int
260     Code: str
261     Name: str
262     Farbe: str
263     IsBio: bool
264     IsPapier: bool
265     IsRest: bool
266     IsWertstoff: bool
267     Bemerkung: None
268     Aktiv: None
269     IsSchadstoff: None
270 
271 
272 class ContainerType(TypedDict):
273     GefaesstarifArtenId: int
274     BescheidText: None
275     BescheidTextLeerungsgebuehr: None
276     Bezeichnung: str
277     GefaesstarifArtVerwenden: bool
278     GefaesstarifArtVerwendenAbfallkalender: bool
279     Bemerkung: None
280     Volumen: Capacity
281     Abfallart: WasteType
282     # Abfuhrrhythmus: Abfuhrrhythmus
283 
284 
285 class CollectionPlan(TypedDict):
286     AbfuhrplaeneId: int
287     Jahr: int
288     GefaesstarifArt: ContainerType
289     # AbfallartenObj: Abfuhrrhythmus
290 
291 
292 class CollectionRes(TypedDict):
293     AbfuhrtermineId: int
294     Termin: str
295     Abfuhrplan: CollectionPlan
296 
297 
298 class CollectionsRes(TypedDict):
299     d: List[CollectionRes]
300
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py b/custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py
--- a/custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py
+++ b/custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py
@@ -77,6 +77,12 @@
         "api_url": "https://biedenkopfmzv.buergerportal.digital/api",
         "operator": "biedenkopf",
     },
+    {
+        "title": "Bürgerportal Bedburg",
+        "url": "https://www.bedburg.de/",
+        "api_url": "https://buerger-portal-bedburg.azurewebsites.net",
+        "operator": "bedburg",
+    },
 ]

{"golden_diff": "diff --git a/custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py b/custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py\n--- a/custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py\n+++ b/custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py\n@@ -77,6 +77,12 @@\n         \"api_url\": \"https://biedenkopfmzv.buergerportal.digital/api\",\n         \"operator\": \"biedenkopf\",\n     },\n+    {\n+        \"title\": \"B\u00fcrgerportal Bedburg\",\n+        \"url\": \"https://www.bedburg.de/\",\n+        \"api_url\": \"https://buerger-portal-bedburg.azurewebsites.net\",\n+        \"operator\": \"bedburg\",\n+    },\n ]\n", "issue": "[Source Request]: \n### Municipality / Region\n\nBedburg\n\n### Collection Calendar Webpage\n\nhttps://buerger-portal-bedburg.azurewebsites.net/calendar/\n\n### Example Address\n\nAm Schirkerhof 2, Bedburg, Broich\n\n### Collection Data Format\n\nSomething different (add to additional information)\n\n### Additional Information\n\nThe city of Bedburg change its Waste Collection Provider. Thus the old Abfall+ Source doesn't work anymore. The new provider is Drekopf. Unfortunately they do not use the existing Drekopf API. The Webpage provided by Drekopf has multiple outputs. You can download the calendar via PDF, XLS, CSV and ICS, but i can't find the url. The underlying API seems to be C-Trace, but when calling this API, credentials are requested. Maybe i am missing something...\n", "before_files": [{"content": "import datetime\nimport re\nfrom dataclasses import dataclass\nfrom typing import List, Literal, Optional, TypedDict, Union\n\nimport requests\nfrom waste_collection_schedule import Collection  # type: ignore[attr-defined]\n\nTITLE = \"B\u00fcrgerportal\"\nURL = \"https://www.c-trace.de\"\nDESCRIPTION = \"Source for waste collection in multiple service areas.\"\n\n\ndef EXTRA_INFO():\n    return [{\"title\": s[\"title\"], \"url\": s[\"url\"]} for s in SERVICE_MAP]\n\n\nTEST_CASES = {\n    \"Cochem-Zell\": {\n        \"operator\": \"cochem_zell\",\n        \"district\": \"Bullay\",\n        \"subdistrict\": \"Bullay\",\n        \"street\": \"Layenweg\",\n        \"number\": 3,\n    },\n    \"Alb-Donau\": {\n        \"operator\": \"alb_donau\",\n        \"district\": \"Blaubeuren\",\n        \"street\": \"Alberstra\u00dfe\",\n        \"number\": 3,\n    },\n    \"Biedenkopf\": {\n        \"operator\": \"biedenkopf\",\n        \"district\": \"Biedenkopf\",\n        \"subdistrict\": \"Breidenstein\",\n        \"street\": \"Auf dem Hammer\",\n        \"number\": 1,\n    },\n}\n\nICON_MAP = {\n    \"mobil\": \"mdi:truck\",\n    \"bio\": \"mdi:leaf\",\n    \"papier\": \"mdi:package-variant\",\n    \"verpackung\": \"mdi:recycle\",\n    \"gelb\": \"mdi:recycle\",\n    \"lvp\": \"mdi:recycle\",\n    \"rest\": \"mdi:trash-can\",\n    \"gruen\": \"mdi:forest\",\n    \"gr\u00fcn\": \"mdi:forest\",\n    \"baum\": \"mdi:forest\",\n    \"schnitt\": \"mdi:forest\",\n    \"schad\": \"mdi:biohazard\",\n}\nAPI_HEADERS = {\n    \"Accept\": \"application/json, text/plain;q=0.5\",\n    \"Cache-Control\": \"no-cache\",\n}\nOperator = Literal[\"cochem_zell\", \"alb_donau\", \"biedenkopf\"]\n\nSERVICE_MAP = [\n    {\n        \"title\": \"KV Cochem-Zell\",\n        \"url\": \"https://www.cochem-zell-online.de/\",\n        \"api_url\": \"https://buerger-portal-cochemzell.azurewebsites.net/api\",\n        \"operator\": \"cochem_zell\",\n    },\n    {\n        \"title\": \"Abfallwirtschaft Alb-Donau-Kreis\",\n        \"url\": \"https://www.aw-adk.de/\",\n        \"api_url\": \"https://buerger-portal-albdonaukreisabfallwirtschaft.azurewebsites.net/api\",\n        \"operator\": \"alb_donau\",\n    },\n    {\n        \"title\": \"MZV Biedenkopf\",\n        \"url\": \"https://mzv-biedenkopf.de/\",\n        \"api_url\": \"https://biedenkopfmzv.buergerportal.digital/api\",\n        \"operator\": \"biedenkopf\",\n    },\n]\n\n\n# This datalcass is used for adding entries to a set and remove duplicate entries.\n# The default `Collection` extends the standard dict and thus is not hashable.\n@dataclass(frozen=True, eq=True)\nclass CollectionEntry:\n    date: datetime.date\n    waste_type: str\n    icon: Optional[str]\n\n    def export(self) -> Collection:\n        return Collection(self.date, self.waste_type, self.icon)\n\n\ndef quote_none(value: Optional[str]) -> str:\n    if value is None:\n        return \"null\"\n\n    return f\"'{value}'\"\n\n\ndef get_api_map():\n    return {s[\"operator\"]: s[\"api_url\"] for s in SERVICE_MAP}\n\n\nclass Source:\n    def __init__(\n        self,\n        operator: Operator,\n        district: str,\n        street: str,\n        subdistrict: Optional[str] = None,\n        number: Union[int, str, None] = None,\n        show_volume: bool = False,\n    ):\n        self.api_url = get_api_map()[operator]\n        self.district = district\n        self.subdistrict = subdistrict\n        self.street = street\n        self.number = number\n        self.show_volume = show_volume\n\n    def fetch(self) -> list[Collection]:\n        session = requests.session()\n        session.headers.update(API_HEADERS)\n\n        year = datetime.datetime.now().year\n        entries: set[CollectionEntry] = set()\n\n        district_id = self.fetch_district_id(session)\n        street_id = self.fetch_street_id(session, district_id)\n        # Eventually verify house number in the future\n\n        params = {\n            \"$expand\": \"Abfuhrplan,Abfuhrplan/GefaesstarifArt/Abfallart,Abfuhrplan/GefaesstarifArt/Volumen\",\n            \"$orderby\": \"Abfuhrplan/GefaesstarifArt/Abfallart/Name,Abfuhrplan/GefaesstarifArt/Volumen/VolumenWert\",\n            \"orteId\": district_id,\n            \"strassenId\": street_id,\n            \"jahr\": year,\n        }\n\n        if self.number:\n            params[\"hausNr\"] = (f\"'{self.number}'\",)\n\n        res = session.get(\n            f\"{self.api_url}/AbfuhrtermineAbJahr\",\n            params=params,\n        )\n        res.raise_for_status()\n        payload: CollectionsRes = res.json()\n\n        date_regex = re.compile(r\"\\d+\")\n\n        for collection in payload[\"d\"]:\n            if date_match := re.search(date_regex, collection[\"Termin\"]):\n                timestamp = float(date_match.group())\n                date = datetime.datetime.utcfromtimestamp(timestamp / 1000).date()\n                waste_type = collection[\"Abfuhrplan\"][\"GefaesstarifArt\"][\"Abfallart\"][\n                    \"Name\"\n                ]\n                icon = None\n\n                for icon_type, tested_icon in ICON_MAP.items():\n                    if icon_type.lower() in waste_type.lower():\n                        icon = tested_icon\n\n                if self.show_volume:\n                    volume = int(\n                        collection[\"Abfuhrplan\"][\"GefaesstarifArt\"][\"Volumen\"][\n                            \"VolumenWert\"\n                        ]\n                    )\n                    waste_type = f\"{waste_type} ({volume} l)\"\n\n                entries.add(CollectionEntry(date, waste_type, icon))\n\n        if len(entries) == 0:\n            raise ValueError(\n                \"No collections found! Please verify that your configuration is correct.\"\n            )\n\n        return [entry.export() for entry in entries]\n\n    def fetch_district_id(self, session: requests.Session) -> int:\n        res = session.get(\n            f\"{self.api_url}/OrteMitOrtsteilen\",\n            headers=API_HEADERS,\n        )\n        res.raise_for_status()\n        payload: DistrictsRes = res.json()\n\n        try:\n            return next(\n                entry[\"OrteId\"]\n                for entry in payload[\"d\"]\n                if entry[\"Ortsname\"] == self.district\n                and entry[\"Ortsteilname\"] == self.subdistrict\n            )\n        except StopIteration:\n            raise ValueError(\n                \"District id cannot be fetched. \"\n                \"Please make sure that you entered a subdistrict if there is a comma on the website.\"\n            )\n\n    def fetch_street_id(self, session: requests.Session, district_id: int):\n        res = session.get(\n            f\"{self.api_url}/Strassen\",\n            params={\n                \"$filter\": f\"Ort/OrteId eq {district_id} and OrtsteilName eq {quote_none(self.subdistrict)}\",\n                \"$orderby\": \"Name asc\",\n            },\n            headers=API_HEADERS,\n        )\n        res.raise_for_status()\n        payload: StreetsRes = res.json()\n\n        try:\n            return next(\n                entry[\"StrassenId\"]\n                for entry in payload[\"d\"]\n                if entry[\"Name\"] == self.street\n            )\n        except StopIteration:\n            raise ValueError(\n                \"Street ID cannot be fetched. Please verify your configuration.\"\n            )\n\n\n# Typed dictionaries for the API\n# Automatically generated using https://pytyper.dev/\n\n\nclass DistrictRes(TypedDict):\n    OrteId: int\n    Ortsname: str\n    Ortsteilname: Optional[str]\n\n\nclass DistrictsRes(TypedDict):\n    d: List[DistrictRes]\n\n\nclass StreetRes(TypedDict):\n    StrassenId: int\n    Name: str\n    Plz: str\n\n\nclass StreetsRes(TypedDict):\n    d: List[StreetRes]\n\n\nclass Capacity(TypedDict):\n    VolumenId: int\n    VolumenWert: str\n\n\nclass WasteType(TypedDict):\n    AbfallartenId: int\n    Code: str\n    Name: str\n    Farbe: str\n    IsBio: bool\n    IsPapier: bool\n    IsRest: bool\n    IsWertstoff: bool\n    Bemerkung: None\n    Aktiv: None\n    IsSchadstoff: None\n\n\nclass ContainerType(TypedDict):\n    GefaesstarifArtenId: int\n    BescheidText: None\n    BescheidTextLeerungsgebuehr: None\n    Bezeichnung: str\n    GefaesstarifArtVerwenden: bool\n    GefaesstarifArtVerwendenAbfallkalender: bool\n    Bemerkung: None\n    Volumen: Capacity\n    Abfallart: WasteType\n    # Abfuhrrhythmus: Abfuhrrhythmus\n\n\nclass CollectionPlan(TypedDict):\n    AbfuhrplaeneId: int\n    Jahr: int\n    GefaesstarifArt: ContainerType\n    # AbfallartenObj: Abfuhrrhythmus\n\n\nclass CollectionRes(TypedDict):\n    AbfuhrtermineId: int\n    Termin: str\n    Abfuhrplan: CollectionPlan\n\n\nclass CollectionsRes(TypedDict):\n    d: List[CollectionRes]\n", "path": "custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py"}], "after_files": [{"content": "import datetime\nimport re\nfrom dataclasses import dataclass\nfrom typing import List, Literal, Optional, TypedDict, Union\n\nimport requests\nfrom waste_collection_schedule import Collection  # type: ignore[attr-defined]\n\nTITLE = \"B\u00fcrgerportal\"\nURL = \"https://www.c-trace.de\"\nDESCRIPTION = \"Source for waste collection in multiple service areas.\"\n\n\ndef EXTRA_INFO():\n    return [{\"title\": s[\"title\"], \"url\": s[\"url\"]} for s in SERVICE_MAP]\n\n\nTEST_CASES = {\n    \"Cochem-Zell\": {\n        \"operator\": \"cochem_zell\",\n        \"district\": \"Bullay\",\n        \"subdistrict\": \"Bullay\",\n        \"street\": \"Layenweg\",\n        \"number\": 3,\n    },\n    \"Alb-Donau\": {\n        \"operator\": \"alb_donau\",\n        \"district\": \"Blaubeuren\",\n        \"street\": \"Alberstra\u00dfe\",\n        \"number\": 3,\n    },\n    \"Biedenkopf\": {\n        \"operator\": \"biedenkopf\",\n        \"district\": \"Biedenkopf\",\n        \"subdistrict\": \"Breidenstein\",\n        \"street\": \"Auf dem Hammer\",\n        \"number\": 1,\n    },\n}\n\nICON_MAP = {\n    \"mobil\": \"mdi:truck\",\n    \"bio\": \"mdi:leaf\",\n    \"papier\": \"mdi:package-variant\",\n    \"verpackung\": \"mdi:recycle\",\n    \"gelb\": \"mdi:recycle\",\n    \"lvp\": \"mdi:recycle\",\n    \"rest\": \"mdi:trash-can\",\n    \"gruen\": \"mdi:forest\",\n    \"gr\u00fcn\": \"mdi:forest\",\n    \"baum\": \"mdi:forest\",\n    \"schnitt\": \"mdi:forest\",\n    \"schad\": \"mdi:biohazard\",\n}\nAPI_HEADERS = {\n    \"Accept\": \"application/json, text/plain;q=0.5\",\n    \"Cache-Control\": \"no-cache\",\n}\nOperator = Literal[\"cochem_zell\", \"alb_donau\", \"biedenkopf\"]\n\nSERVICE_MAP = [\n    {\n        \"title\": \"KV Cochem-Zell\",\n        \"url\": \"https://www.cochem-zell-online.de/\",\n        \"api_url\": \"https://buerger-portal-cochemzell.azurewebsites.net/api\",\n        \"operator\": \"cochem_zell\",\n    },\n    {\n        \"title\": \"Abfallwirtschaft Alb-Donau-Kreis\",\n        \"url\": \"https://www.aw-adk.de/\",\n        \"api_url\": \"https://buerger-portal-albdonaukreisabfallwirtschaft.azurewebsites.net/api\",\n        \"operator\": \"alb_donau\",\n    },\n    {\n        \"title\": \"MZV Biedenkopf\",\n        \"url\": \"https://mzv-biedenkopf.de/\",\n        \"api_url\": \"https://biedenkopfmzv.buergerportal.digital/api\",\n        \"operator\": \"biedenkopf\",\n    },\n    {\n        \"title\": \"B\u00fcrgerportal Bedburg\",\n        \"url\": \"https://www.bedburg.de/\",\n        \"api_url\": \"https://buerger-portal-bedburg.azurewebsites.net\",\n        \"operator\": \"bedburg\",\n    },\n]\n\n\n# This datalcass is used for adding entries to a set and remove duplicate entries.\n# The default `Collection` extends the standard dict and thus is not hashable.\n@dataclass(frozen=True, eq=True)\nclass CollectionEntry:\n    date: datetime.date\n    waste_type: str\n    icon: Optional[str]\n\n    def export(self) -> Collection:\n        return Collection(self.date, self.waste_type, self.icon)\n\n\ndef quote_none(value: Optional[str]) -> str:\n    if value is None:\n        return \"null\"\n\n    return f\"'{value}'\"\n\n\ndef get_api_map():\n    return {s[\"operator\"]: s[\"api_url\"] for s in SERVICE_MAP}\n\n\nclass Source:\n    def __init__(\n        self,\n        operator: Operator,\n        district: str,\n        street: str,\n        subdistrict: Optional[str] = None,\n        number: Union[int, str, None] = None,\n        show_volume: bool = False,\n    ):\n        self.api_url = get_api_map()[operator]\n        self.district = district\n        self.subdistrict = subdistrict\n        self.street = street\n        self.number = number\n        self.show_volume = show_volume\n\n    def fetch(self) -> list[Collection]:\n        session = requests.session()\n        session.headers.update(API_HEADERS)\n\n        year = datetime.datetime.now().year\n        entries: set[CollectionEntry] = set()\n\n        district_id = self.fetch_district_id(session)\n        street_id = self.fetch_street_id(session, district_id)\n        # Eventually verify house number in the future\n\n        params = {\n            \"$expand\": \"Abfuhrplan,Abfuhrplan/GefaesstarifArt/Abfallart,Abfuhrplan/GefaesstarifArt/Volumen\",\n            \"$orderby\": \"Abfuhrplan/GefaesstarifArt/Abfallart/Name,Abfuhrplan/GefaesstarifArt/Volumen/VolumenWert\",\n            \"orteId\": district_id,\n            \"strassenId\": street_id,\n            \"jahr\": year,\n        }\n\n        if self.number:\n            params[\"hausNr\"] = (f\"'{self.number}'\",)\n\n        res = session.get(\n            f\"{self.api_url}/AbfuhrtermineAbJahr\",\n            params=params,\n        )\n        res.raise_for_status()\n        payload: CollectionsRes = res.json()\n\n        date_regex = re.compile(r\"\\d+\")\n\n        for collection in payload[\"d\"]:\n            if date_match := re.search(date_regex, collection[\"Termin\"]):\n                timestamp = float(date_match.group())\n                date = datetime.datetime.utcfromtimestamp(timestamp / 1000).date()\n                waste_type = collection[\"Abfuhrplan\"][\"GefaesstarifArt\"][\"Abfallart\"][\n                    \"Name\"\n                ]\n                icon = None\n\n                for icon_type, tested_icon in ICON_MAP.items():\n                    if icon_type.lower() in waste_type.lower():\n                        icon = tested_icon\n\n                if self.show_volume:\n                    volume = int(\n                        collection[\"Abfuhrplan\"][\"GefaesstarifArt\"][\"Volumen\"][\n                            \"VolumenWert\"\n                        ]\n                    )\n                    waste_type = f\"{waste_type} ({volume} l)\"\n\n                entries.add(CollectionEntry(date, waste_type, icon))\n\n        if len(entries) == 0:\n            raise ValueError(\n                \"No collections found! Please verify that your configuration is correct.\"\n            )\n\n        return [entry.export() for entry in entries]\n\n    def fetch_district_id(self, session: requests.Session) -> int:\n        res = session.get(\n            f\"{self.api_url}/OrteMitOrtsteilen\",\n            headers=API_HEADERS,\n        )\n        res.raise_for_status()\n        payload: DistrictsRes = res.json()\n\n        try:\n            return next(\n                entry[\"OrteId\"]\n                for entry in payload[\"d\"]\n                if entry[\"Ortsname\"] == self.district\n                and entry[\"Ortsteilname\"] == self.subdistrict\n            )\n        except StopIteration:\n            raise ValueError(\n                \"District id cannot be fetched. \"\n                \"Please make sure that you entered a subdistrict if there is a comma on the website.\"\n            )\n\n    def fetch_street_id(self, session: requests.Session, district_id: int):\n        res = session.get(\n            f\"{self.api_url}/Strassen\",\n            params={\n                \"$filter\": f\"Ort/OrteId eq {district_id} and OrtsteilName eq {quote_none(self.subdistrict)}\",\n                \"$orderby\": \"Name asc\",\n            },\n            headers=API_HEADERS,\n        )\n        res.raise_for_status()\n        payload: StreetsRes = res.json()\n\n        try:\n            return next(\n                entry[\"StrassenId\"]\n                for entry in payload[\"d\"]\n                if entry[\"Name\"] == self.street\n            )\n        except StopIteration:\n            raise ValueError(\n                \"Street ID cannot be fetched. Please verify your configuration.\"\n            )\n\n\n# Typed dictionaries for the API\n# Automatically generated using https://pytyper.dev/\n\n\nclass DistrictRes(TypedDict):\n    OrteId: int\n    Ortsname: str\n    Ortsteilname: Optional[str]\n\n\nclass DistrictsRes(TypedDict):\n    d: List[DistrictRes]\n\n\nclass StreetRes(TypedDict):\n    StrassenId: int\n    Name: str\n    Plz: str\n\n\nclass StreetsRes(TypedDict):\n    d: List[StreetRes]\n\n\nclass Capacity(TypedDict):\n    VolumenId: int\n    VolumenWert: str\n\n\nclass WasteType(TypedDict):\n    AbfallartenId: int\n    Code: str\n    Name: str\n    Farbe: str\n    IsBio: bool\n    IsPapier: bool\n    IsRest: bool\n    IsWertstoff: bool\n    Bemerkung: None\n    Aktiv: None\n    IsSchadstoff: None\n\n\nclass ContainerType(TypedDict):\n    GefaesstarifArtenId: int\n    BescheidText: None\n    BescheidTextLeerungsgebuehr: None\n    Bezeichnung: str\n    GefaesstarifArtVerwenden: bool\n    GefaesstarifArtVerwendenAbfallkalender: bool\n    Bemerkung: None\n    Volumen: Capacity\n    Abfallart: WasteType\n    # Abfuhrrhythmus: Abfuhrrhythmus\n\n\nclass CollectionPlan(TypedDict):\n    AbfuhrplaeneId: int\n    Jahr: int\n    GefaesstarifArt: ContainerType\n    # AbfallartenObj: Abfuhrrhythmus\n\n\nclass CollectionRes(TypedDict):\n    AbfuhrtermineId: int\n    Termin: str\n    Abfuhrplan: CollectionPlan\n\n\nclass CollectionsRes(TypedDict):\n    d: List[CollectionRes]\n", "path": "custom_components/waste_collection_schedule/waste_collection_schedule/source/buergerportal_de.py"}]}