From 383395625655fd79763388c259422513990015a7 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:51:33 +0000 Subject: [PATCH 01/18] fix: London Borough Redbridge fix: #1836 - fix: London Borough Redbridge --- .../councils/LondonBoroughRedbridge.py | 60 ++++++------------- 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/LondonBoroughRedbridge.py b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughRedbridge.py index ec055e9992..9d8ef6136f 100644 --- a/uk_bin_collection/uk_bin_collection/councils/LondonBoroughRedbridge.py +++ b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughRedbridge.py @@ -61,7 +61,7 @@ def parse_data(self, page: str, **kwargs) -> dict: address_link.send_keys(Keys.ENTER) - address_results = wait.until( + wait.until( EC.presence_of_element_located( (By.CLASS_NAME, "your-collection-schedule-container") ) @@ -71,16 +71,16 @@ def parse_data(self, page: str, **kwargs) -> dict: soup = BeautifulSoup(driver.page_source, features="html.parser") data = {"bins": []} - # Get the current month and year - current_month = datetime.now().month - current_year = datetime.now().year - # Function to extract collection data def extract_collection_data(collection_div, collection_type): if collection_div: date_element = ( - collection_div.find(class_="recycling-collection-day-numeric") - or collection_div.find(class_="refuse-collection-day-numeric") + collection_div.find( + class_="refuse-garden-collection-day-numeric" + ) + or collection_div.find( + class_="recycling-garden-collection-day-numeric" + ) or collection_div.find(class_="garden-collection-day-numeric") ) month_element = ( @@ -94,27 +94,14 @@ def extract_collection_data(collection_div, collection_type): collection_month = month_element.get_text(strip=True) # Combine month, date, and year into a string - date_string = ( - f"{collection_date} {collection_month} {current_year}" - ) + date_string = f"{collection_date} {collection_month}" try: # Convert the date string to a datetime object - collection_date_obj = datetime.strptime( + formatted_date = datetime.strptime( date_string, "%d %B %Y" - ) - - # Check if the month is ahead of the current month - if collection_date_obj.month >= current_month: - # If the month is ahead, use the current year - formatted_date = collection_date_obj.strftime( - date_format - ) - else: - # If the month is before the current month, use the next year - formatted_date = collection_date_obj.replace( - year=current_year + 1 - ).strftime(date_format) + ).strftime(date_format) + # Create a dictionary for each collection entry dict_data = { "type": collection_type, @@ -128,25 +115,12 @@ def extract_collection_data(collection_div, collection_type): # Handle the case where the date format is invalid formatted_date = "Invalid Date Format" - # Extract Recycling collection data - extract_collection_data( - soup.find( - class_="container-fluid RegularCollectionDay" - ).find_next_sibling("div"), - "Recycling", - ) - - # Extract Refuse collection data - for refuse_div in soup.find_all( - class_="container-fluid RegularCollectionDay" - ): - extract_collection_data(refuse_div, "Refuse") - - # Extract Garden Waste collection data - extract_collection_data( - soup.find(class_="container-fluid gardenwasteCollectionDay"), - "Garden Waste", - ) + container_fluid = soup.find_all(class_="container-fluid") + for container in container_fluid: + collection_type = container.find("h3").get_text().strip() + collections = container.find_all(class_="bs3-col-sm-2") + for collection in collections: + extract_collection_data(collection, collection_type) # Print the extracted data except Exception as e: From ce62fa2e2295671806940f9a61b7a16ba99ddf4b Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:09:42 +0000 Subject: [PATCH 02/18] fix: Harborough District Council fix: #1831 - Harborough District Council --- .../councils/HarboroughDistrictCouncil.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/HarboroughDistrictCouncil.py b/uk_bin_collection/uk_bin_collection/councils/HarboroughDistrictCouncil.py index 71b05210eb..0d8e96b2a6 100644 --- a/uk_bin_collection/uk_bin_collection/councils/HarboroughDistrictCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/HarboroughDistrictCouncil.py @@ -19,15 +19,20 @@ def parse_data(self, page: str, **kwargs) -> dict: check_uprn(user_uprn) bindata = {"bins": []} - URI = "https://harborough.fccenvironment.co.uk/detail-address" + URI1 = "https://harborough.fccenvironment.co.uk/" + URI2 = "https://harborough.fccenvironment.co.uk/detail-address" + + # Make the GET request + session = requests.session() + response = session.get( + URI1, verify=False + ) # Initialize session state (cookies) required by URI2 + response.raise_for_status() # Validate session initialization - headers = { - "Content-Type": "application/json", - "User-Agent": "Mozilla/5.0", - "Referer": "https://harborough.fccenvironment.co.uk/", - } params = {"Uprn": user_uprn} - response = requests.post(URI, headers=headers, json=params) + + response = session.post(URI2, json=params, verify=False) + response.raise_for_status() # Raise HTTPError for bad status codes soup = BeautifulSoup(response.content, features="html.parser") bin_collection = soup.find( From a46ffb65e83b84f79cb7352810f948d3ff09fcdb Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:11:35 +0000 Subject: [PATCH 03/18] fix: Adding Hammersmith & Fulham fix: #1504 - Adding Hammersmith & Fulham --- uk_bin_collection/tests/input.json | 8 ++ .../LondonBoroughHammersmithandFulham.py | 57 ++++++++++++++ wiki/Councils.md | 77 +++++++++++-------- 3 files changed, 111 insertions(+), 31 deletions(-) create mode 100644 uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py diff --git a/uk_bin_collection/tests/input.json b/uk_bin_collection/tests/input.json index 8e73784656..dbc81f2e76 100755 --- a/uk_bin_collection/tests/input.json +++ b/uk_bin_collection/tests/input.json @@ -1408,6 +1408,14 @@ "wiki_note": "Pass the UPRN. You can find it using [FindMyAddress](https://www.findmyaddress.co.uk/search).", "LAD24CD": "E09000009" }, + "LondonBoroughHammersmithandFulham": { + "postcode": "W12 0BQ", + "url": "https://www.lbhf.gov.uk/", + "wiki_command_url_override": "https://www.lbhf.gov.uk/", + "wiki_name": "Hammersmith & Fulham", + "wiki_note": "Pass only the property postcode", + "LAD24CD": "E09000013" + }, "LondonBoroughHarrow": { "uprn": "100021298754", "url": "https://www.harrow.gov.uk", diff --git a/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py new file mode 100644 index 0000000000..7f399502cb --- /dev/null +++ b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py @@ -0,0 +1,57 @@ +from datetime import datetime + +import requests +from bs4 import BeautifulSoup + +from uk_bin_collection.uk_bin_collection.common import * +from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass + + +# import the wonderful Beautiful Soup and the URL grabber +class CouncilClass(AbstractGetBinDataClass): + """ + Concrete classes have to implement all abstract operations of the + base class. They can also override some operations with a default + implementation. + """ + + def parse_data(self, page: str, **kwargs) -> dict: + + user_postcode = kwargs.get("postcode") + check_uprn(user_postcode) + bindata = {"bins": []} + + user_postcode = user_postcode.strip().replace(" ", "") + + URI = f"https://www.lbhf.gov.uk/bin-recycling-day/results?postcode={user_postcode}" + UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" + session = requests.session() + session.headers.update({"User-Agent": UA}) + # Make the GET request + response = session.get(URI) + response.raise_for_status() + + soup = BeautifulSoup(response.content, features="html.parser") + results = soup.find("div", {"class": "nearest-search-results"}) + ol = results.find("ol") + bin_collections = ol.find_all("a") + for bin_collection in bin_collections: + collection_day = bin_collection.get_text().split(" - ")[0] + collection_type = bin_collection.get_text().split(" - ")[1] + + if days_of_week.get(collection_day) == 0: + collection_day = datetime.now().strftime(date_format) + else: + collection_day = get_next_day_of_week(collection_day) + + dict_data = { + "type": collection_type, + "collectionDate": collection_day, + } + bindata["bins"].append(dict_data) + + bindata["bins"].sort( + key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y") + ) + + return bindata diff --git a/wiki/Councils.md b/wiki/Councils.md index bb87a7345d..9ca2775cd0 100644 --- a/wiki/Councils.md +++ b/wiki/Councils.md @@ -76,14 +76,12 @@ This document is still a work in progress, don't worry if your council isn't lis - [Chorley](#chorley) - [Colchester](#colchester) - [Conwy](#conwy) -- [Copeland](#copeland) - [Cornwall](#cornwall) - [Cotswold](#cotswold) - [Coventry](#coventry) - [Crawley](#crawley) - [Croydon](#croydon) - [Cumberland](#cumberland) -- [Cumberland](#cumberland) - [Dacorum](#dacorum) - [Darlington Borough Council](#darlington-borough-council) - [Dartford](#dartford) @@ -111,6 +109,7 @@ This document is still a work in progress, don't worry if your council isn't lis - [East Staffordshire](#east-staffordshire) - [East Suffolk](#east-suffolk) - [Eastleigh](#eastleigh) +- [Eden District (Westmorland and Furness)](#eden-district-(westmorland-and-furness)) - [City of Edinburgh](#city-of-edinburgh) - [Elmbridge](#elmbridge) - [Enfield](#enfield) @@ -171,7 +170,9 @@ This document is still a work in progress, don't worry if your council isn't lis - [City of Lincoln](#city-of-lincoln) - [Lisburn and Castlereagh](#lisburn-and-castlereagh) - [Liverpool](#liverpool) +- [Camden](#camden) - [Ealing](#ealing) +- [Hammersmith & Fulham](#hammersmith-&-fulham) - [Harrow](#harrow) - [Havering](#havering) - [Hounslow](#hounslow) @@ -628,13 +629,14 @@ Note: You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/searc ### Bexley ```commandline -python collect_data.py BexleyCouncil https://waste.bexley.gov.uk/waste -s -u XXXXXXXX +python collect_data.py BexleyCouncil https://waste.bexley.gov.uk/waste -s -u XXXXXXXX -w http://HOST:PORT/ ``` Additional parameters: - `-s` - skip get URL - `-u` - UPRN +- `-w` - remote Selenium web driver URL (required for Home Assistant) -Note: Provide your UPRN. Use [FindMyAddress](https://www.findmyaddress.co.uk/search) to locate it. +Note: Provide your UPRN. Use [FindMyAddress](https://www.findmyaddress.co.uk/search) to locate it. This parser requires a Selenium webdriver. --- @@ -1156,17 +1158,6 @@ Note: Use [FindMyAddress](https://www.findmyaddress.co.uk/search) to find your U --- -### Copeland -```commandline -python collect_data.py CopelandBoroughCouncil https://www.copeland.gov.uk -u XXXXXXXX -``` -Additional parameters: -- `-u` - UPRN - -Note: *****This has now been replaced by Cumberland Council**** - ---- - ### Cornwall ```commandline python collect_data.py CornwallCouncil https://www.cornwall.gov.uk/my-area/ -s -u XXXXXXXX @@ -1229,18 +1220,6 @@ Note: Pass the house number and postcode in their respective parameters. This pa --- -### Cumberland -```commandline -python collect_data.py CumberlandAllerdaleCouncil https://www.allerdale.gov.uk -p "XXXX XXX" -n XX -``` -Additional parameters: -- `-p` - postcode -- `-n` - house number - -Note: Pass the house number and postcode in their respective parameters. - ---- - ### Cumberland ```commandline python collect_data.py CumberlandCouncil https://www.cumberland.gov.uk/bins-recycling-and-street-cleaning/waste-collections/bin-collection-schedule -u XXXXXXXX @@ -1579,6 +1558,17 @@ Note: Pass the UPRN. You can find it using [FindMyAddress](https://www.findmyadd --- +### Eden District (Westmorland and Furness) +```commandline +python collect_data.py EdenDistrictCouncil https://my.eden.gov.uk/myeden.aspx -u XXXXXXXX +``` +Additional parameters: +- `-u` - UPRN + +Note: For Eden area addresses within Westmorland and Furness. Provide your UPRN. You can find your UPRN using [FindMyAddress](https://www.findmyaddress.co.uk/search). Note: This returns collection days (e.g., 'Wednesday') rather than specific dates. + +--- + ### City of Edinburgh ```commandline python collect_data.py EdinburghCityCouncil https://www.edinburgh.gov.uk -s -p "XXXX XXX" -n XX @@ -2314,6 +2304,19 @@ Note: You will need to use [FindMyAddress](https://www.findmyaddress.co.uk/searc --- +### Camden +```commandline +python collect_data.py LondonBoroughCamdenCouncil https://environmentservices.camden.gov.uk/property -s -u XXXXXXXX -p "XXXX XXX" +``` +Additional parameters: +- `-s` - skip get URL +- `-u` - UPRN +- `-p` - postcode + +Note: Pass the property ID as UPRN. Find your property at https://www.camden.gov.uk/check-collection-day then use the property ID from the URL (e.g., https://environmentservices.camden.gov.uk/property/5063139). + +--- + ### Ealing ```commandline python collect_data.py LondonBoroughEaling https://www.ealing.gov.uk/site/custom_scripts/WasteCollectionWS/home/FindCollection -s -u XXXXXXXX @@ -2326,6 +2329,17 @@ Note: Pass the UPRN. You can find it using [FindMyAddress](https://www.findmyadd --- +### Hammersmith & Fulham +```commandline +python collect_data.py LondonBoroughHammersmithandFulham https://www.lbhf.gov.uk/ -p "XXXX XXX" +``` +Additional parameters: +- `-p` - postcode + +Note: Pass only the property postcode + +--- + ### Harrow ```commandline python collect_data.py LondonBoroughHarrow https://www.harrow.gov.uk -u XXXXXXXX @@ -3468,7 +3482,7 @@ Note: Follow the instructions [here](https://www.southlanarkshire.gov.uk/info/20 ### South Norfolk ```commandline -python collect_data.py SouthNorfolkCouncil https://www.southnorfolkandbroadland.gov.uk/rubbish-recycling/south-norfolk-bin-collection-day-finder -s -u XXXXXXXX +python collect_data.py SouthNorfolkCouncil https://area.southnorfolkandbroadland.gov.uk/FindAddress -s -u XXXXXXXX ``` Additional parameters: - `-s` - skip get URL @@ -4257,12 +4271,13 @@ Note: Provide your UPRN. You can find it using [FindMyAddress](https://www.findm ### Wirral ```commandline -python collect_data.py WirralCouncil https://www.wirral.gov.uk -u XXXXXXXX +python collect_data.py WirralCouncil https://www.wirral.gov.uk -p "XXXX XXX" -w http://HOST:PORT/ ``` Additional parameters: -- `-u` - UPRN +- `-p` - postcode +- `-w` - remote Selenium web driver URL (required for Home Assistant) -Note: In the `uprn` field, enter your street name and suburb separated by a comma (e.g., 'Vernon Avenue,Seacombe'). +Note: Pass your postcode and house number. --- From a9f94d487b949d07c5a1eaec7f0fdb2c9791c92b Mon Sep 17 00:00:00 2001 From: Wiki GitHub Action Date: Mon, 2 Feb 2026 23:15:27 +0000 Subject: [PATCH 04/18] docs: Update Councils.md from input.json --- wiki/Councils.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/wiki/Councils.md b/wiki/Councils.md index 9ca2775cd0..b093fc540a 100644 --- a/wiki/Councils.md +++ b/wiki/Councils.md @@ -131,6 +131,7 @@ This document is still a work in progress, don't worry if your council isn't lis - [Gedling](#gedling) - [Glasgow City](#glasgow-city) - [Gloucester](#gloucester) +- [Gosport Borough Council](#gosport-borough-council) - [Google Calendar (Public)](#google-calendar-(public)) - [Gravesham](#gravesham) - [Great Yarmouth](#great-yarmouth) @@ -1828,6 +1829,18 @@ Note: Pass the house number, postcode, and UPRN in their respective parameters. --- +### Gosport Borough Council +```commandline +python collect_data.py GosportBoroughCouncil https://www.gosport.gov.uk/refuserecyclingdays -s -p "XXXX XXX" +``` +Additional parameters: +- `-s` - skip get URL +- `-p` - postcode + +Note: Pass the postcode parameter. This parser uses the Supatrak API. + +--- + ### Google Calendar (Public) ```commandline python collect_data.py GooglePublicCalendarCouncil https://calendar.google.com/calendar/ical/0d775884b4db6a7bae5204f06dae113c1a36e505b25991ebc27c6bd42edf5b5e%40group.calendar.google.com/public/basic.ics From 5dccfdcb5233e0aaa2878f29bce0efbcb47a6bea Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:25:12 +0000 Subject: [PATCH 05/18] fix: HarboroughDistrictCouncil fix: HarboroughDistrictCouncil --- .../councils/HarboroughDistrictCouncil.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/HarboroughDistrictCouncil.py b/uk_bin_collection/uk_bin_collection/councils/HarboroughDistrictCouncil.py index 45f875ac85..84cc8155a7 100644 --- a/uk_bin_collection/uk_bin_collection/councils/HarboroughDistrictCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/HarboroughDistrictCouncil.py @@ -34,7 +34,7 @@ def parse_data(self, page: str, **kwargs) -> dict: response.raise_for_status() # Validate session initialization params = {"Uprn": user_uprn} - response = requests.post(URI, headers=headers, data=params, verify=False) + response = session.post(URI2, data=params, verify=False) # Check for service errors if response.status_code == 502: @@ -43,20 +43,20 @@ def parse_data(self, page: str, **kwargs) -> dict: f"This is a temporary issue with the council's waste collection system. " f"Please try again later." ) - + response.raise_for_status() soup = BeautifulSoup(response.content, features="html.parser") bin_collection = soup.find( "div", {"class": "blocks block-your-next-scheduled-bin-collection-days"} ) - + if bin_collection is None: raise ValueError( f"Could not find bin collection data for UPRN {user_uprn}. " "The council website may have changed or the UPRN may be invalid." ) - + lis = bin_collection.find_all("li") for li in lis: try: From 9c45fde83ae182f88033b0d2580ecff5865b65ff Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:28:56 +0000 Subject: [PATCH 06/18] fix: LondonBoroughHammersmithandFulham --- .../councils/LondonBoroughHammersmithandFulham.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py index 7f399502cb..f08e0983e6 100644 --- a/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py +++ b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py @@ -18,7 +18,7 @@ class CouncilClass(AbstractGetBinDataClass): def parse_data(self, page: str, **kwargs) -> dict: user_postcode = kwargs.get("postcode") - check_uprn(user_postcode) + check_postcode(user_postcode) bindata = {"bins": []} user_postcode = user_postcode.strip().replace(" ", "") From add90fcf0ea0390456ce545744321cf0b47aca10 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:31:52 +0000 Subject: [PATCH 07/18] fix: LondonBoroughHammersmithandFulham --- .../councils/LondonBoroughHammersmithandFulham.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py index f08e0983e6..0080b057ea 100644 --- a/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py +++ b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHammersmithandFulham.py @@ -35,11 +35,14 @@ def parse_data(self, page: str, **kwargs) -> dict: results = soup.find("div", {"class": "nearest-search-results"}) ol = results.find("ol") bin_collections = ol.find_all("a") + + today = datetime.now().strftime("%A") + for bin_collection in bin_collections: collection_day = bin_collection.get_text().split(" - ")[0] collection_type = bin_collection.get_text().split(" - ")[1] - if days_of_week.get(collection_day) == 0: + if days_of_week.get(collection_day) == days_of_week.get(today): collection_day = datetime.now().strftime(date_format) else: collection_day = get_next_day_of_week(collection_day) From be071e21781774aa54ae9d786a4ceadb7eb9b235 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Thu, 5 Feb 2026 22:06:34 +0000 Subject: [PATCH 08/18] fix: Powys Council fix: #1846 - Powys Council --- .../uk_bin_collection/councils/PowysCouncil.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/PowysCouncil.py b/uk_bin_collection/uk_bin_collection/councils/PowysCouncil.py index ad6ff46e35..b5e3498892 100644 --- a/uk_bin_collection/uk_bin_collection/councils/PowysCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/PowysCouncil.py @@ -92,7 +92,7 @@ def parse_data(self, page: str, **kwargs) -> dict: # General rubbish collection dates general_rubbish_section = soup.find( - "h3", string="General Rubbish / Wheelie bin" + "div", class_="bdl-card bdl-card--refuse" ) general_rubbish_dates = [ li.text for li in general_rubbish_section.find_next("ul").find_all("li") @@ -102,13 +102,14 @@ def parse_data(self, page: str, **kwargs) -> dict: dict_data = { "type": "General Rubbish / Wheelie bin", "collectionDate": datetime.strptime( - remove_ordinal_indicator_from_date_string(date), "%d %B %Y" + remove_ordinal_indicator_from_date_string(date.split(" (")[0]), + "%A %d %B %Y", ).strftime(date_format), } data["bins"].append(dict_data) # Recycling and food waste collection dates - recycling_section = soup.find("h3", string="Recycling and Food Waste") + recycling_section = soup.find("div", class_="bdl-card bdl-card--recycling") recycling_dates = [ li.text for li in recycling_section.find_next("ul").find_all("li") ] @@ -117,13 +118,14 @@ def parse_data(self, page: str, **kwargs) -> dict: dict_data = { "type": "Recycling and Food Waste", "collectionDate": datetime.strptime( - remove_ordinal_indicator_from_date_string(date), "%d %B %Y" + remove_ordinal_indicator_from_date_string(date.split(" (")[0]), + "%A %d %B %Y", ).strftime(date_format), } data["bins"].append(dict_data) # Garden waste collection dates - garden_waste_section = soup.find("h3", string="Garden Waste") + garden_waste_section = soup.find("div", class_="bdl-card bdl-card--garden") garden_waste_dates = [ li.text for li in garden_waste_section.find_next("ul").find_all("li") ] @@ -132,7 +134,10 @@ def parse_data(self, page: str, **kwargs) -> dict: dict_data = { "type": "Garden Waste", "collectionDate": datetime.strptime( - remove_ordinal_indicator_from_date_string(date), "%d %B %Y" + remove_ordinal_indicator_from_date_string( + date.split(" (")[0] + ), + "%A %d %B %Y", ).strftime(date_format), } data["bins"].append(dict_data) From c1c70e666aaff564685cfa503a5e5ce0caa18eee Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:11:07 +0000 Subject: [PATCH 09/18] fix: Mid Suffolk District Council fix: #1845 - Mid Suffolk District Council --- .../uk_bin_collection/councils/MidSuffolkDistrictCouncil.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uk_bin_collection/uk_bin_collection/councils/MidSuffolkDistrictCouncil.py b/uk_bin_collection/uk_bin_collection/councils/MidSuffolkDistrictCouncil.py index aca3a58b3a..1797f9c4dd 100644 --- a/uk_bin_collection/uk_bin_collection/councils/MidSuffolkDistrictCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/MidSuffolkDistrictCouncil.py @@ -118,6 +118,8 @@ def parse_data(self, page: str, **kwargs) -> dict: # Collect text in p excluding the strong tag date_str = (p_tag.get_text()).split(":")[1] + if " - " in date_str: + date_str = date_str.split(" - ")[1] collection_date = datetime.strptime(date_str, "%a %d %b %Y") From 473c40b4e33f135854edd1db15c951cfb6efca10 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:15:42 +0000 Subject: [PATCH 10/18] fix: Bromley Borough Council fix: #1851 Bromley Borough Council --- .../uk_bin_collection/councils/BromleyBoroughCouncil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/BromleyBoroughCouncil.py b/uk_bin_collection/uk_bin_collection/councils/BromleyBoroughCouncil.py index 30a9bd82f4..d19bddb29b 100644 --- a/uk_bin_collection/uk_bin_collection/councils/BromleyBoroughCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/BromleyBoroughCouncil.py @@ -30,7 +30,8 @@ def parse_data(self, page: str, **kwargs) -> dict: data = {"bins": []} # Get our initial session running - driver = create_webdriver(web_driver, headless, None, __name__) + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + driver = create_webdriver(web_driver, headless, user_agent, __name__) driver.get(kwargs.get("url")) wait = WebDriverWait(driver, 30) From ef6ec89d7da9b780b620b7ea72eb76236c216b71 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:46:11 +0000 Subject: [PATCH 11/18] fix: Wakefield City Council fix: #1853 - Wakefield City Council --- .../councils/WakefieldCityCouncil.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/WakefieldCityCouncil.py b/uk_bin_collection/uk_bin_collection/councils/WakefieldCityCouncil.py index 24770ca1b8..87b10b0818 100644 --- a/uk_bin_collection/uk_bin_collection/councils/WakefieldCityCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/WakefieldCityCouncil.py @@ -16,9 +16,10 @@ def parse_data(self, page: str, **kwargs) -> dict: try: # Create Selenium webdriver headless = kwargs.get("headless") - driver = create_webdriver( - kwargs.get("web_driver"), headless, None, __name__ - ) + web_driver = kwargs.get("web_driver") + + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + driver = create_webdriver(web_driver, headless, user_agent, __name__) driver.get(kwargs.get("url")) # This URL format also works: @@ -38,7 +39,9 @@ def parse_data(self, page: str, **kwargs) -> dict: "div", {"class": "c-content-section_body"} ).find_all( "div", - class_=lambda x: x and "tablet:l-col-fb-4" in x and "u-mt-10" in x + class_=lambda x: x + and "tablet:l-col-fb-4" in x + and "u-mt-10" in x, ) for row in rows: From db52375e400e526d477f80f18cedde53a83fdb05 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:19:04 +0000 Subject: [PATCH 12/18] fix: Redcar and Cleveland Council fix: #1848 - Redcar and Cleveland Council --- .../uk_bin_collection/councils/RedcarandClevelandCouncil.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/RedcarandClevelandCouncil.py b/uk_bin_collection/uk_bin_collection/councils/RedcarandClevelandCouncil.py index 8639306680..d2271ff790 100644 --- a/uk_bin_collection/uk_bin_collection/councils/RedcarandClevelandCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/RedcarandClevelandCouncil.py @@ -43,11 +43,11 @@ def parse_data(self, page: str, **kwargs) -> dict: for item in addresses if item.get("name", "").startswith(user_paon) ), - None, + addresses[1]["place_id"] if addresses[1] else None, ) # print(addresses) - # print(place_id) + # print(f"PlaceID - {place_id}") URI = ( f"https://api.eu.recollect.net/api/places/{place_id}/services/50006/events" From 1747edf05c2ae9dae3e80e8627f029dfe3fa2a24 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:53:37 +0000 Subject: [PATCH 13/18] fix: Barking & Dagenham fix: #1855 - Barking & Dagenham --- .../councils/BarkingDagenham.py | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/BarkingDagenham.py b/uk_bin_collection/uk_bin_collection/councils/BarkingDagenham.py index 7fe52600c6..b007a1e655 100644 --- a/uk_bin_collection/uk_bin_collection/councils/BarkingDagenham.py +++ b/uk_bin_collection/uk_bin_collection/councils/BarkingDagenham.py @@ -47,23 +47,7 @@ def parse_data(self, page: str, **kwargs) -> dict: # Close popup if it exists driver.switch_to.active_element.send_keys(Keys.ESCAPE) - # Handle cookie banner if present - wait = WebDriverWait(driver, 60) - try: - cookie_button = wait.until( - EC.element_to_be_clickable( - ( - By.CSS_SELECTOR, - ".agree-button.eu-cookie-compliance-secondary-button.button.button--small", - ) - ), - message="Cookie banner not found", - ) - cookie_button.click() - print("Cookie banner clicked.") - time.sleep(1) # Brief pause to let banner disappear - except (TimeoutException, NoSuchElementException): - print("No cookie banner appeared or selector failed.") + wait = WebDriverWait(driver, 10) # Enter postcode print("Looking for postcode input...") @@ -84,7 +68,7 @@ def parse_data(self, page: str, **kwargs) -> dict: EC.element_to_be_clickable((By.ID, "address")), message="Address dropdown not found", ) - + dropdown = Select(address_select) found = False From 3aed4ccc0877646f87cc83db6c8a937fd17ddb7c Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:14:32 +0000 Subject: [PATCH 14/18] fix: Cumberland Council fix: #1858 - Cumberland Council --- .../councils/CumberlandCouncil.py | 74 +++++-------------- 1 file changed, 19 insertions(+), 55 deletions(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/CumberlandCouncil.py b/uk_bin_collection/uk_bin_collection/councils/CumberlandCouncil.py index a665024cea..f351faadd8 100644 --- a/uk_bin_collection/uk_bin_collection/councils/CumberlandCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/CumberlandCouncil.py @@ -1,3 +1,5 @@ +from datetime import date + import requests from bs4 import BeautifulSoup @@ -29,64 +31,26 @@ def parse_data(self, page: str, **kwargs) -> dict: if not content_region: return bindata - # Parse the text content to extract collection dates - text_content = content_region.get_text() - lines = [line.strip() for line in text_content.split('\n') if line.strip()] - - current_month = None - current_year = None - i = 0 - - # Determine the year range from the page header - year_2026 = "2026" in text_content - - while i < len(lines): - line = lines[i] - - # Check if this is a month name - if line in ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December"]: - current_month = line - # Determine year based on month and context - if year_2026: - current_year = "2026" if line in ["January", "February"] else "2025" - else: - current_year = str(datetime.now().year) - i += 1 - continue - - # Check if this is a day number (1-31) - if line.isdigit() and 1 <= int(line) <= 31 and current_month: - day = line - # Next line should be the bin type - if i + 1 < len(lines): - bin_type = lines[i + 1] - - # Skip the subtype line (Refuse/Recycling detail) - if i + 2 < len(lines) and lines[i + 2] in ["Refuse", "Recycling"]: - i += 1 - - # Parse the date - try: - date_str = f"{day} {current_month} {current_year}" - collection_date = datetime.strptime(date_str, "%d %B %Y") - - dict_data = { - "type": bin_type, - "collectionDate": collection_date.strftime(date_format), - } - bindata["bins"].append(dict_data) - except ValueError: - pass - - i += 2 - continue - - i += 1 + lis = content_region.find_all("li") + for li in lis: + collection_day = li.find("span", class_="waste-collection__day--day") + collection_type_str = li.find("span", class_="waste-collection__day--type") + + collection_date = collection_day.find("time")["datetime"] + + collection_type = collection_type_str.text + + collection_date = datetime.strptime(collection_date, "%Y-%m-%d") + + dict_data = { + "type": collection_type.strip(), + "collectionDate": collection_date.strftime(date_format), + } + bindata["bins"].append(dict_data) # Sort by collection date bindata["bins"].sort( - key=lambda x: datetime.strptime(x.get("collectionDate"), "%d/%m/%Y") + key=lambda x: datetime.strptime(x.get("collectionDate"), date_format) ) return bindata From 36573c35153454dc9aba5b0ec2e6eeea8e6b347b Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:22:59 +0000 Subject: [PATCH 15/18] fix: North East Derbyshire District Council fix: #1861 - North East Derbyshire District Council --- .../councils/NorthEastDerbyshireDistrictCouncil.py | 1 + 1 file changed, 1 insertion(+) diff --git a/uk_bin_collection/uk_bin_collection/councils/NorthEastDerbyshireDistrictCouncil.py b/uk_bin_collection/uk_bin_collection/councils/NorthEastDerbyshireDistrictCouncil.py index 7290687f5d..3855acd1be 100644 --- a/uk_bin_collection/uk_bin_collection/councils/NorthEastDerbyshireDistrictCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/NorthEastDerbyshireDistrictCouncil.py @@ -28,6 +28,7 @@ def parse_data(self, page: str, **kwargs) -> dict: data = {"bins": []} user_uprn = kwargs.get("uprn") + user_uprn = str(user_uprn).zfill(12) user_postcode = kwargs.get("postcode") web_driver = kwargs.get("web_driver") headless = kwargs.get("headless") From c5cf578ecb5594be8e3e8d11d3466c6684ee80c5 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:13:53 +0000 Subject: [PATCH 16/18] fix: Leeds City Council fix: #1864 - Leeds City Council --- uk_bin_collection/tests/input.json | 5 +- .../councils/LeedsCityCouncil.py | 121 +++++------------- wiki/Councils.md | 7 +- 3 files changed, 38 insertions(+), 95 deletions(-) diff --git a/uk_bin_collection/tests/input.json b/uk_bin_collection/tests/input.json index 92f1180a6d..c3923c353a 100755 --- a/uk_bin_collection/tests/input.json +++ b/uk_bin_collection/tests/input.json @@ -1344,14 +1344,11 @@ "LAD24CD": "E07000121" }, "LeedsCityCouncil": { - "house_number": "1", - "postcode": "LS6 2SE", "skip_get_url": true, "uprn": "72506983", "url": "https://www.leeds.gov.uk/residents/bins-and-recycling/check-your-bin-day", - "web_driver": "http://selenium:4444", "wiki_name": "Leeds", - "wiki_note": "Pass the house number, postcode, and UPRN. This parser requires a Selenium webdriver.", + "wiki_note": "Pass the UPRN.", "LAD24CD": "E08000035" }, "LeicesterCityCouncil": { diff --git a/uk_bin_collection/uk_bin_collection/councils/LeedsCityCouncil.py b/uk_bin_collection/uk_bin_collection/councils/LeedsCityCouncil.py index fe0daa49e8..8237b18bee 100644 --- a/uk_bin_collection/uk_bin_collection/councils/LeedsCityCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/LeedsCityCouncil.py @@ -1,14 +1,5 @@ -import urllib.request from datetime import datetime -import pandas as pd -from bs4 import BeautifulSoup -from selenium.webdriver.common.by import By -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.ui import Select -from selenium.webdriver.support.wait import WebDriverWait - from uk_bin_collection.uk_bin_collection.common import * from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass @@ -22,92 +13,50 @@ class CouncilClass(AbstractGetBinDataClass): def parse_data(self, page: str, **kwargs) -> dict: driver = None + data = {"bins": []} try: - """ - Parse council provided CSVs to get the latest bin collections for address - """ - user_uprn = kwargs.get("uprn") - user_postcode = kwargs.get("postcode") - web_driver = kwargs.get("web_driver") - headless = kwargs.get("headless") check_uprn(user_uprn) - check_postcode(user_postcode) - # Create Selenium webdriver - page = f"https://www.leeds.gov.uk/residents/bins-and-recycling/check-your-bin-day" - - driver = create_webdriver(web_driver, headless, None, __name__) - driver.get(page) - - wait = WebDriverWait(driver, 60) - postcode_box = wait.until( - EC.element_to_be_clickable( - ( - By.XPATH, - "//input[@id='postcode']", - ) - ) - ) - postcode_box.send_keys(user_postcode) - postcode_btn_present = wait.until( - EC.presence_of_element_located( - ( - By.XPATH, - "//button[contains(text(),'Look up Address')]", - ) - ) - ) - postcode_btn_present.send_keys(Keys.RETURN) + URI = "https://api.leeds.gov.uk/public/waste/v1/BinsDays" - dropdown_present = wait.until( - EC.presence_of_element_located( - ( - By.XPATH, - '//option[contains(text(),"Select an address")]/parent::select', - ) - ) - ) + startDate = datetime.now() + endDate = (startDate + timedelta(weeks=8)).strftime("%Y-%m-%d") + startDate = startDate.strftime("%Y-%m-%d") + + params = { + "uprn": user_uprn, + "startDate": startDate, + "endDate": endDate, + } + + headers = { + "ocp-apim-subscription-key": "ad8dd80444fe45fcad376f82cf9a5ab4", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", + } - dropdown_select = Select(dropdown_present) + # print(params) - dropdown_select.select_by_value(user_uprn) + # Send GET request + response = requests.get(URI, params=params, headers=headers) - result = wait.until( - EC.presence_of_element_located( - ( - By.XPATH, - "//div[@class='lcc-bins']", - ) + print(response.content) + + collections = json.loads(response.content) + + for collection in collections: + + collectionDate = datetime.strptime( + collection["date"], "%Y-%m-%dT%H:%M:%S" ) - ) - - data = {"bins": []} # dictionary for data - soup = BeautifulSoup( - result.get_attribute("innerHTML"), features="html.parser" - ) - - bin_sections = soup.select("div.lcc-bin:not(.lcc-bin--calendar)") - - for section in bin_sections: - h3_text = section.find("h3").get_text(strip=True) - bin_type = h3_text.split()[0] # e.g., 'Black', 'Brown', 'Green' - - # Find all
  • elements inside the bin days list - date_elements = section.select("div.lcc-bin__days li") - for li in date_elements: - raw_date = li.get_text(strip=True) - if not raw_date: - continue - try: - formatted_date = datetime.strptime( - raw_date, "%A %d %b %Y" - ).strftime(date_format) - data["bins"].append( - {"type": bin_type, "collectionDate": formatted_date} - ) - except ValueError: - print(f"Skipping unparseable date: {raw_date}") + + data["bins"].append( + { + "type": collection["type"], + "collectionDate": collectionDate.strftime(date_format), + } + ) + except Exception as e: # Here you can log the exception if needed print(f"An error occurred: {e}") diff --git a/wiki/Councils.md b/wiki/Councils.md index b093fc540a..ba7a0577f4 100644 --- a/wiki/Councils.md +++ b/wiki/Councils.md @@ -2233,16 +2233,13 @@ Note: Pass the house number and postcode in their respective parameters. ### Leeds ```commandline -python collect_data.py LeedsCityCouncil https://www.leeds.gov.uk/residents/bins-and-recycling/check-your-bin-day -s -u XXXXXXXX -p "XXXX XXX" -n XX -w http://HOST:PORT/ +python collect_data.py LeedsCityCouncil https://www.leeds.gov.uk/residents/bins-and-recycling/check-your-bin-day -s -u XXXXXXXX ``` Additional parameters: - `-s` - skip get URL - `-u` - UPRN -- `-p` - postcode -- `-n` - house number -- `-w` - remote Selenium web driver URL (required for Home Assistant) -Note: Pass the house number, postcode, and UPRN. This parser requires a Selenium webdriver. +Note: Pass the UPRN. --- From 56dfaa5429c7bc35a47f0c717ee1a0634ba310b7 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:14:09 +0000 Subject: [PATCH 17/18] fix: London Borough Havering fix: #1863 - London Borough Havering --- .../uk_bin_collection/councils/LondonBoroughHavering.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHavering.py b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHavering.py index e0daae44b2..97cb2d50ab 100644 --- a/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHavering.py +++ b/uk_bin_collection/uk_bin_collection/councils/LondonBoroughHavering.py @@ -21,9 +21,9 @@ def parse_data(self, page: str, **kwargs) -> dict: check_uprn(user_uprn) bindata = {"bins": []} - URI = "https://lbhapiprod.azure-api.net" + URI = "https://api-prd.havering.gov.uk" endpoint = f"{URI}/whitespace/GetCollectionByUprnAndDate" - subscription_key = "2ea6a75f9ea34bb58d299a0c9f84e72e" + subscription_key = "545bcf53c9094dfd980dd9da72b0514d" # Get today's date in 'YYYY-MM-DD' format collection_date = datetime.now().strftime("%Y-%m-%d") From 8f2823f93d10ac4ade20e22e12942aa6ed334456 Mon Sep 17 00:00:00 2001 From: m26dvd <31007572+m26dvd@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:25:28 +0000 Subject: [PATCH 18/18] fix: Eastleigh Borough Council fix: #1867 - Eastleigh Borough Council --- .../uk_bin_collection/councils/EastleighBoroughCouncil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uk_bin_collection/uk_bin_collection/councils/EastleighBoroughCouncil.py b/uk_bin_collection/uk_bin_collection/councils/EastleighBoroughCouncil.py index 71ea375fb7..b81c94bfc0 100644 --- a/uk_bin_collection/uk_bin_collection/councils/EastleighBoroughCouncil.py +++ b/uk_bin_collection/uk_bin_collection/councils/EastleighBoroughCouncil.py @@ -23,7 +23,8 @@ def parse_data(self, page: str, **kwargs) -> dict: headless = kwargs.get("headless") web_driver = kwargs.get("web_driver") url = f"https://www.eastleigh.gov.uk/waste-bins-and-recycling/collection-dates/your-waste-bin-and-recycling-collections?uprn={uprn}" - driver = create_webdriver(web_driver, headless, None, __name__) + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + driver = create_webdriver(web_driver, headless, user_agent, __name__) driver.get(url) wait = WebDriverWait(driver, 10)