Repository: mampfes/hacs_waste_collection_schedule

Issue:
[Bug]: Error when configuring Recycle Coach
### I Have A Problem With:

A specific source

### What's Your Problem

Receiving error when configuring recycle coach for Vaughan Ontarion (unlisted city, but supported by recycle coach)


### Source (if relevant)

recycle coach

### Logs

```Shell
Logger: waste_collection_schedule.source_shell
Source: custom_components/waste_collection_schedule/waste_collection_schedule/source_shell.py:136
Integration: waste_collection_schedule (documentation)
First occurred: 3:10:21 PM (1 occurrences)
Last logged: 3:10:21 PM

fetch failed for source Recycle Coach: Traceback (most recent call last): File "/config/custom_components/waste_collection_schedule/waste_collection_schedule/source_shell.py", line 134, in fetch entries = self._source.fetch() ^^^^^^^^^^^^^^^^^^^^ File "/config/custom_components/waste_collection_schedule/waste_collection_schedule/source/recyclecoach_com.py", line 159, in fetch self._lookup_zones() File "/config/custom_components/waste_collection_schedule/waste_collection_schedule/source/recyclecoach_com.py", line 135, in _lookup_zones for zone_res in zone_data["results"]: ~~~~~~~~~^^^^^^^^^^^ KeyError: 'results'
```


### Relevant Configuration

```YAML
waste_collection_schedule:
  sources:
  - name: recyclecoach_com 
    calendar_title: Waste Collection Schedule
    args:
      street: <<street address in lowercase>>
      city: <<city in lowercase>>
      state: <<province in lowercase>>
```


### 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:
I can not reproduce this issue as all our test cases pass and return collection dates.
Our test cases:
```json
{
    "Default": {"street": "2242 grinstead drive", "city": "louisville", "state": "KY"},
    "Problematic City Lookup": {
        "street": "2202 E Florence Dr",
        "city": "Tucson",
        "state": "AZ",
        "district_id": "TUC",
        "project_id": "532",
    },
    "olympia": {
        "street": "1003 Lybarger St NE",
        "city": "Olympia",
        "state": "Washington",
    },
    "newark": {"street": "24 Townsend Rd", "city": "Newark", "state": "Delaware"},
    "albuquerque": {
        "street": "1505 Silver Ave SE",
        "city": "Albuquerque",
        "state": "New Mexico",
    },
    "london ontario": {
        "street": "1065 Sunningdale Rd E",
        "city": "London",
        "state": "Ontario",
    },
    "london ontario with districtID": {
        "street": "1065 Sunningdale Rd E",
        "city": "London",
        "state": "Ontario",
        "project_id": "528",
        "district_id": "CityofLondon",
        "zone_id": "zone-z547",
    },
    "aurora ontario": {
        "street": "123 Cranberry Lane",
        "city": "Aurora",
        "state": "Ontario",
    },
}
```
have you tried to restart HA or waited for one day (for new fetch) maybe it was a temporal problem.
If not:
- could you try one of the examples above?
- check that your provided parameters are correct
I used the Aurora test case and it runs without an issue.  When I use the same format for Vaughan, which I know is using recycle coach, it fails with the error above.

Is there a way to get you the address for use in testing and/or a way for me to validate the address better?  I can't find API docs on how to query an address.
Adding.  When I add a district and project, I'm getting the following error.

```Logger: homeassistant.setup
Source: setup.py:332
First occurred: 5:26:44 PM (1 occurrences)
Last logged: 5:26:44 PM

Error during setup of component waste_collection_schedule
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/setup.py", line 332, in _async_setup_component
    result = await task
             ^^^^^^^^^^
  File "/config/custom_components/waste_collection_schedule/__init__.py", line 109, in async_setup
    api.add_source_shell(
  File "/config/custom_components/waste_collection_schedule/__init__.py", line 202, in add_source_shell
    SourceShell.create(
  File "/config/custom_components/waste_collection_schedule/waste_collection_schedule/source_shell.py", line 196, in create
    source = source_module.Source(**source_args)  # type: ignore
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/waste_collection_schedule/waste_collection_schedule/source/recyclecoach_com.py", line 91, in __init__
    self.project_id = self._format_key(project_id) if project_id else None
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/waste_collection_schedule/waste_collection_schedule/source/recyclecoach_com.py", line 99, in _format_key
    return param.upper().strip()
           ^^^^^^^^^^^
AttributeError: 'int' object has no attribute 'upper'
```

Resolving patch:
diff --git a/custom_components/waste_collection_schedule/waste_collection_schedule/source/recyclecoach_com.py b/custom_components/waste_collection_schedule/waste_collection_schedule/source/recyclecoach_com.py
--- a/custom_components/waste_collection_schedule/waste_collection_schedule/source/recyclecoach_com.py
+++ b/custom_components/waste_collection_schedule/waste_collection_schedule/source/recyclecoach_com.py
@@ -2,7 +2,7 @@
 from datetime import datetime
 
 import requests
-from waste_collection_schedule import Collection
+from waste_collection_schedule import Collection  # type: ignore[attr-defined]
 
 TITLE = "Recycle Coach"
 DESCRIPTION = "Source loader for recyclecoach.com"
@@ -38,6 +38,7 @@
     },
     {"title": "London (ON)", "url": "https://london.ca/", "country": "ca"},
     {"title": "Aurora (ON)", "url": "https://www.aurora.ca/", "country": "ca"},
+    {"title": "Vaughan (ON)", "url": "https://www.vaughan.ca/", "country": "ca"},
 ]
 
 TEST_CASES = {
@@ -78,6 +79,11 @@
         "city": "Aurora",
         "state": "Ontario",
     },
+    "Vaughan, Ontario, Canada": {  # https://app.my-waste.mobi/widget/576-Vaughan/home.php
+        "street": "Main St",
+        "city": "Vaughan",
+        "state": "Ontario",
+    },
 }
 
 
@@ -89,14 +95,14 @@ def __init__(
         self.city = self._format_key(city)
         self.state = self._format_key(state)
         self.project_id = self._format_key(project_id) if project_id else None
-        self.district_id = district_id.strip() if district_id else None
+        self.district_id = str(district_id).strip() if district_id else None
 
         self.zone_id = zone_id  # uses lowercase z's, not sure if matters
         self.stage = 0
 
     def _format_key(self, param):
         """Get rid of ambiguity in caps/spacing."""
-        return param.upper().strip()
+        return str(param).upper().strip()
 
     def _lookup_city(self):
         city_finder = f"https://recyclecoach.com/wp-json/rec/v1/cities?find={self.city}, {self.state}"
@@ -114,7 +120,6 @@ def _lookup_city(self):
                 )
 
         elif len(city_data["cities"]) > 1:
-
             for city in city_data["cities"]:
                 if city["city_nm"].upper() == self.city.upper():
                     self.project_id = city["project_id"]
@@ -128,10 +133,35 @@ def _lookup_city(self):
                 "Could not determine district or project, Debug here to find your discrict and project_id"
             )
 
+    def _lookup_zones_with_geo(self):
+        pos_finder = f"https://api-city.recyclecoach.com/geo/address?address={self.street}&uuid=ecdb86fe-e42d-4a9d-94d6-7057777ef283&project_id={self.project_id}&district_id={self.district_id}"
+        res = requests.get(pos_finder)
+        lat = None
+        pos_data = res.json()
+        for pos_res in pos_data:
+            streetpart = self._format_key(pos_res["address"]).split(",")[0]
+
+            if streetpart in self.street:
+                lat = pos_res["lat"]
+                lng = pos_res["lng"]
+                break
+
+        if not lat:
+            raise Exception("Unable to find zone")
+
+        zone_finder = f"https://pkg.my-waste.mobi/get_zones?project_id={self.project_id}&district_id={self.district_id}&lat={lat}&lng={lng}"
+        res = requests.get(zone_finder)
+        zone_data = {z["prompt_id"]: "z" + z["zone_id"] for z in res.json()}
+        self.zone_id = self._build_zone_string(zone_data)
+
+        return self.zone_id
+
     def _lookup_zones(self):
         zone_finder = f"https://api-city.recyclecoach.com/zone-setup/address?sku={self.project_id}&district={self.district_id}&prompt=undefined&term={self.street}"
         res = requests.get(zone_finder)
         zone_data = res.json()
+        if "results" not in zone_data:
+            return self._lookup_zones_with_geo()
         for zone_res in zone_data["results"]:
             streetpart = self._format_key(zone_res["address"]).split(",")[0]
 


Test patch: