Repository: mampfes/hacs_waste_collection_schedule

Issue:
[Bug]: awb_bad_kreuznach not working
### I Have A Problem With:

A specific source

### What's Your Problem

New to HA and I am trying to implement the awb_bad_kreuznach_de source but i get no entries in the calendar.
When using a different source (e.g. awb_mainz_bingen_de) everything works instantly so i would think that the base config in HA is correct.

The source was updated some month ago, maybe something has changed again. I know that the provider was quite late with updating the calendar due to some problems they had if i remember correctly.

(https://github.com/mampfes/hacs_waste_collection_schedule/pull/1036/commits/283af0c0273724ef87612f69f1c9e1c44593d59a)


### Source (if relevant)

awb_bad_kreuznach_de

### Logs

```Shell
no relevant logs
```


### Relevant Configuration

```YAML
waste_collection_schedule:
  sources:
    - name: awb_bad_kreuznach_de
      args:
        ort: "Roxheim"
        
sensor:
  - platform: waste_collection_schedule
    name: Papiertonne
    types:
      - Altpapier
```


### Checklist Source Error

- [X] Use the example parameters for your source (often available in the documentation) (don't forget to restart Home Assistant after changing the configuration)
- [X] Checked that the website of your service provider is still working
- [X] Tested my attributes on the service provider website (if possible)
- [X] I have tested with the latest version of the integration (master) (for HACS in the 3 dot menu of the integration click on "Redownload" and choose master as version)

### Checklist Sensor Error

- [ ] Checked in the Home Assistant Calendar tab if the event names match the types names (if types argument is used)

### Required

- [X] I have searched past (closed AND opened) issues to see if this bug has already been reported, and it hasn't been.
- [X] I understand that people give their precious time for free, and thus I've done my very best to make this problem as easy as possible to investigate.


Discussion:

Resolving patch:
diff --git a/custom_components/waste_collection_schedule/waste_collection_schedule/source/awb_bad_kreuznach_de.py b/custom_components/waste_collection_schedule/waste_collection_schedule/source/awb_bad_kreuznach_de.py
--- a/custom_components/waste_collection_schedule/waste_collection_schedule/source/awb_bad_kreuznach_de.py
+++ b/custom_components/waste_collection_schedule/waste_collection_schedule/source/awb_bad_kreuznach_de.py
@@ -1,19 +1,26 @@
-import datetime
-import json
 import logging
+from datetime import date, datetime, timedelta
 
 import requests
+from dateutil.rrule import DAILY, rrule
 from waste_collection_schedule import Collection  # type: ignore[attr-defined]
 
 TITLE = "AWB Bad Kreuznach"
 DESCRIPTION = "Source for AWB Bad Kreuznach."
-URL = "https://app.awb-bad-kreuznach.de/"
-NEW_URL = "https://blupassionsystem.de/city/rest/garbageregion/filterRegion"
+URL = "https://blupassionsystem.de/city/rest/garbageregion/filterRegion"
 TEST_CASES = {
     "Hargesheim": {"ort": "Hargesheim"},
-    "Bad Kreuznach": {"ort": "Bad Kreuznach", "strasse": "adalbert-stifter-straße", "nummer": "3"},
-    "Bad Kreuznach OT Bad Münster-Ebernburg": {"ort": "Bad Kreuznach OT Bad Münster-Ebernburg", "strasse": "am Götzenfels"},
+    "Bad Kreuznach": {
+        "ort": "Bad Kreuznach",
+        "strasse": "adalbert-stifter-straße",
+        "nummer": "3",
+    },
+    "Bad Kreuznach OT Bad Münster-Ebernburg": {
+        "ort": "Bad Kreuznach OT Bad Münster-Ebernburg",
+        "strasse": "am Götzenfels",
+    },
     "Stromberg": {"ort": "Stromberg", "stadtteil": "Schindeldorf"},
+    "Altenbamberg": {"ort": "Altenbamberg"},
 }
 
 TYPES = ("restmuell", "bio", "wert", "papier")
@@ -32,7 +39,9 @@
 
 
 def compare_str(a: str, b: str):
-    return a.lower().replace(" ", "").replace("-", "") == b.lower().replace("-", "").replace(" ", "")
+    return a.lower().replace(" ", "").replace("-", "") == b.lower().replace(
+        "-", ""
+    ).replace(" ", "")
 
 
 class Source:
@@ -42,11 +51,19 @@ def __init__(self, ort, strasse=None, nummer=None, stadtteil=None):
         self._strasse = strasse if strasse else None
         self._nummer = str(nummer) if nummer else None
 
-    def fetch_new_app(self):
-        params = {"active": True, "appId": 44, "hugeDataInfo": "", "regionId": None,
-                  "cityId": None, "streetId": None, "partOfCityId": None, "districtId": None}
+    def fetch(self):
+        params = {
+            "active": True,
+            "appId": 44,
+            "hugeDataInfo": "",
+            "regionId": None,
+            "cityId": None,
+            "streetId": None,
+            "partOfCityId": None,
+            "districtId": None,
+        }
 
-        r = requests.post(NEW_URL, json=params)
+        r = requests.post(URL, json=params)
         r.raise_for_status()
         data = r.json()
 
@@ -55,7 +72,7 @@ def fetch_new_app(self):
         if "regions" in data["data"]:
             params["regionId"] = data["data"]["regions"][0]["id"]
 
-        r = requests.post(NEW_URL, json=params)
+        r = requests.post(URL, json=params)
         r.raise_for_status()
         data = r.json()
 
@@ -69,14 +86,14 @@ def fetch_new_app(self):
         if not found:
             raise ValueError("Ort not found")
 
-        r = requests.post(NEW_URL, json=params)
+        r = requests.post(URL, json=params)
         r.raise_for_status()
         data = r.json()
 
         if data["data"]["partOfCitys"] != []:
-            if self._stadtteil == None:
+            if self._stadtteil is None:
                 raise ValueError("Stadtteil required")
-        elif self._stadtteil != None:
+        elif self._stadtteil is not None:
             LOGGER.warning("stadtteil provided but not needed")
 
         if data["data"]["partOfCitys"]:
@@ -91,14 +108,14 @@ def fetch_new_app(self):
             if not found:
                 raise ValueError("Stadtteil not found")
 
-        r = requests.post(NEW_URL, json=params)
+        r = requests.post(URL, json=params)
         r.raise_for_status()
         data = r.json()
 
         if data["data"]["streets"] != []:
-            if self._strasse == None:
+            if self._strasse is None:
                 raise ValueError("strasse required")
-        elif self._strasse != None:
+        elif self._strasse is not None:
             LOGGER.warning("strasse provided but not needed")
 
         if data["data"]["streets"]:
@@ -111,14 +128,14 @@ def fetch_new_app(self):
             if not found:
                 raise ValueError("Strasse not found")
 
-        r = requests.post(NEW_URL, json=params)
+        r = requests.post(URL, json=params)
         r.raise_for_status()
         data = r.json()
 
         if data["data"]["houseNumbers"] != []:
-            if self._nummer == None:
+            if self._nummer is None:
                 raise ValueError("nummer required")
-        elif self._nummer != None:
+        elif self._nummer is not None:
             LOGGER.warning("nummer provided but not needed")
 
         if data["data"]["houseNumbers"]:
@@ -131,80 +148,70 @@ def fetch_new_app(self):
             if not found:
                 raise ValueError("Nummer not found")
 
-        from_time = datetime.datetime.now()
-        from_time = from_time.replace(
-            hour=0, minute=0, second=0, microsecond=0)
-        from_time = from_time - datetime.timedelta(days=1)
+        from_time = datetime.now()
+        from_time = from_time.replace(hour=0, minute=0, second=0, microsecond=0)
+        from_time = from_time - timedelta(days=1)
 
-        to_time = from_time + datetime.timedelta(days=365)
+        to_time = from_time + timedelta(days=365)
 
         params["fromTime"] = int(from_time.timestamp() * 1000)
         params["toTime"] = int(to_time.timestamp() * 1000)
 
         header = {"Accept": "application/json, text/plain, */*"}
 
-        # r = requests.get("https://blupassionsystem.de/city/rest/garbagestation/getNameRegion", params=params, headers=header)
-        # r.raise_for_status()
         r = requests.get(
-            "https://blupassionsystem.de/city/rest/garbagestation/getAllGarbageCalendar", params=params, headers=header)
+            "https://blupassionsystem.de/city/rest/garbageorte/getAllGarbageCalendar",
+            params=params,
+            headers=header,
+        )
         r.raise_for_status()
-
         data = r.json()
 
         if "data" not in data or "calendars" not in data["data"]:
             raise ValueError("Unexpected response")
 
-        entries = []
-        for cal_entry in data["data"]["calendars"]:
-            entries.append(Collection(datetime.date.fromtimestamp(
-                cal_entry["fromDate"] / 1000), cal_entry["name"], ICON_MAP.get(cal_entry["name"])))
-
-        return entries
-
-    def fetch(self):
-        try:
-            to_return = self.fetch_new_app()
-        except Exception as e:
-            LOGGER.warning(
-                "Error fetching new app: with Exception '%s' using old method", e)
-            try:
-                to_return = self.old_fetch()
-            except Exception as e2:
-                LOGGER.warning(
-                    "Error fetching old app: with Exception '%s' raising Exception from new API", e2)
-                raise e
-
-        return to_return
-
-    def old_fetch(self):
-        args = {
-            "ort": self._ort,
-            "strasse": self._strasse,
-            "nummer": self._nummer,
-        }
-
-        # get latitude/longitude file
-        r = requests.post(
-            "https://app.awb-bad-kreuznach.de/api/checkAddress.php", data=args
-        )
-        data = json.loads(r.text)
-
-        # get dates
-        del args["nummer"]
-        args["mode"] = "web"
-        args["lat"] = data["lat"]
-        args["lon"] = data["lon"]
-        r = requests.post(
-            "https://app.awb-bad-kreuznach.de/api/loadDates.php", data=args
-        )
-        data = json.loads(r.text)
+        collection_dates: dict[str, list[date]] = {}
 
         entries = []
-
-        for d in data["termine"]:
-            date = datetime.date.fromisoformat(d["termin"])
-            for type in TYPES:
-                if d[type] != "0":
-                    entries.append(Collection(date, type, date))
+        for cal_entry in data["data"]["calendars"]:
+            collection_dates[cal_entry["name"]] = list(
+                d.date()
+                for d in rrule(
+                    freq=DAILY,
+                    interval=cal_entry["frequency"],
+                    dtstart=datetime.fromtimestamp(cal_entry["fromDate"] / 1000),
+                    until=datetime.fromtimestamp(cal_entry["toDate"] / 1000)
+                    if cal_entry["toDate"]
+                    else None,
+                    count=365 // cal_entry["frequency"],
+                )
+            )
+
+        moved: list[(str, date)] = []
+
+        for holiday in data["data"]["holidayViews"]:
+            holiday_date = datetime.fromtimestamp(holiday["holiday"] / 1000).date()
+            for collection_name, collections in collection_dates.items():
+                for collection in collections:
+                    if (
+                        collection == holiday_date
+                        and not (collection_name, collection) in moved
+                    ):
+                        new_date = datetime.fromtimestamp(
+                            holiday["shiftTo"] / 1000
+                        ).date()
+                        moved.append((collection_name, new_date))
+                        collections[collections.index(collection)] = new_date
+                        break
+
+        for name, collections in collection_dates.items():
+            for collection_date in collections:
+                entries.append(
+                    Collection(
+                        collection_date,
+                        name,
+                        ICON_MAP.get(name),
+                    )
+                )
 
         return entries


Test patch: