Adding impactapps com au (#2204)

* add impact apps source

* fix: reduce duplication in collection creation

* refactor: split location finding from source class

* refactor: move url params to requests params object

* style: fix formatting

* fix: do not filter for unknown events

* fix: add default params for council selection

* reformatting

---------

Co-authored-by: 5ila5 <5ila5@users.noreply.github.com>
Co-authored-by: 5ila5 <38183212+5ila5@users.noreply.github.com>
This commit is contained in:
Morgan
2024-07-03 02:40:53 +10:00
committed by GitHub
parent e9ba6b8078
commit 0136703139
4 changed files with 467 additions and 1 deletions

View File

@@ -30,9 +30,14 @@ Waste collection schedules in the following formats and countries are supported.
- [Armadale (Western Australia)](/doc/source/armadale_wa_gov_au.md) / armadale.wa.gov.au
- [Australian Capital Territory (ACT)](/doc/source/act_gov_au.md) / cityservices.act.gov.au/recycling-and-waste
- [Banyule City Council](/doc/source/banyule_vic_gov_au.md) / banyule.vic.gov.au
- [Baw Baw Shire Council](/doc/source/impactapps_com_au.md) / bawbawshire.vic.gov.au
- [Bayside City Council](/doc/source/impactapps_com_au.md) / bayside.vic.gov.au
- [Bega Valley Shire Council](/doc/source/impactapps_com_au.md) / begavalley.nsw.gov.au
- [Belmont City Council](/doc/source/belmont_wa_gov_au.md) / belmont.wa.gov.au
- [Blacktown City Council (NSW)](/doc/source/blacktown_nsw_gov_au.md) / blacktown.nsw.gov.au
- [Blue Mountains City Council](/doc/source/impactapps_com_au.md) / bmcc.nsw.gov.au
- [Brisbane City Council](/doc/source/brisbane_qld_gov_au.md) / brisbane.qld.gov.au
- [Burwood City Council](/doc/source/impactapps_com_au.md) / burwood.nsw.gov.au
- [Campbelltown City Council (NSW)](/doc/source/campbelltown_nsw_gov_au.md) / campbelltown.nsw.gov.au
- [Cardinia Shire Council](/doc/source/cardinia_vic_gov_au.md) / cardinia.vic.gov.au
- [City of Ballarat](/doc/source/ballarat_vic_gov_au.md) / ballarat.vic.gov.au
@@ -41,17 +46,24 @@ Waste collection schedules in the following formats and countries are supported.
- [City of Greater Geelong](/doc/source/geelongaustralia_com_au.md) / geelongaustralia.com.au
- [City of Kingston](/doc/source/kingston_vic_gov_au.md) / kingston.vic.gov.au
- [City of Onkaparinga Council](/doc/source/onkaparingacity_com.md) / onkaparingacity.com
- [Cowra Council](/doc/source/impactapps_com_au.md) / cowracouncil.com.au
- [Cumberland Council (NSW)](/doc/source/cumberland_nsw_gov_au.md) / cumberland.nsw.gov.au
- [Forbes Shire Council](/doc/source/impactapps_com_au.md) / forbes.nsw.gov.au
- [Frankston City Council](/doc/source/frankston_vic_gov_au.md) / frankston.gov.au
- [Gold Coast City Council](/doc/source/goldcoast_qld_gov_au.md) / goldcoast.qld.gov.au
- [Gwydir Shire Council](/doc/source/impactapps_com_au.md) / gwydir.nsw.gov.au
- [Hobsons Bay City Council](/doc/source/hobsonsbay_vic_gov_au.md) / hobsonsbay.vic.gov.au
- [Hornsby Shire Council](/doc/source/hornsby_nsw_gov_au.md) / hornsby.nsw.gov.au
- [Hume City Council](/doc/source/hume_vic_gov_au.md) / hume.vic.gov.au
- [Impact Apps](/doc/source/impactapps_com_au.md) / impactapps.com.au
- [Inner West Council (NSW)](/doc/source/innerwest_nsw_gov_au.md) / innerwest.nsw.gov.au
- [Ipswich City Council](/doc/source/ipswich_qld_gov_au.md) / ipswich.qld.gov.au
- [Knox City Council](/doc/source/knox_vic_gov_au.md) / knox.vic.gov.au
- [Ku-ring-gai Council](/doc/source/kuringgai_nsw_gov_au.md) / krg.nsw.gov.au
- [Lake Macquarie City Council](/doc/source/lakemac_nsw_gov_au.md) / lakemac.com.au
- [Lithgow City Council](/doc/source/impactapps_com_au.md) / lithgow.nsw.gov.au
- [Livingstone Shire Council](/doc/source/impactapps_com_au.md) / livingstone.qld.gov.au
- [Loddon Shire Council](/doc/source/impactapps_com_au.md) / loddon.vic.gov.au
- [Logan City Council](/doc/source/logan_qld_gov_au.md) / logan.qld.gov.au
- [Macedon Ranges Shire Council](/doc/source/mrsc_vic_gov_au.md) / mrsc.vic.gov.au
- [Mansfield Shire Council](/doc/source/mansfield_vic_gov_au.md) / mansfield.vic.gov.au
@@ -59,21 +71,30 @@ Waste collection schedules in the following formats and countries are supported.
- [Maroondah City Council](/doc/source/maroondah_vic_gov_au.md) / maroondah.vic.gov.au
- [Melton City Council](/doc/source/melton_vic_gov_au.md) / melton.vic.gov.au
- [Merri-bek City Council](/doc/source/merri_bek_vic_gov_au.md) / merri-bek.vic.gov.au
- [Moira Shire Council](/doc/source/impactapps_com_au.md) / moira.vic.gov.au
- [Moree Plains Shire Council](/doc/source/impactapps_com_au.md) / mpsc.nsw.gov.au
- [Moreton Bay](/doc/ics/moretonbay_qld_gov_au.md) / moretonbay.qld.gov.au
- [Mosman Council](/doc/source/mosman_nsw_gov_au.md) / mosman.nsw.gov.au
- [Nillumbik Shire Council](/doc/source/nillumbik_vic_gov_au.md) / nillumbik.vic.gov.au
- [North Adelaide Waste Management Authority](/doc/source/nawma_sa_gov_au.md) / nawma.sa.gov.au
- [Penrith City Council](/doc/source/impactapps_com_au.md) / penrithcity.nsw.gov.au
- [Port Adelaide Enfield, South Australia](/doc/source/portenf_sa_gov_au.md) / ecouncil.portenf.sa.gov.au
- [Port Macquarie Hastings Council](/doc/source/impactapps_com_au.md) / pmhc.nsw.gov.au
- [Port Stephens Council](/doc/source/portstephens_nsw_gov_au.md) / portstephens.nsw.gov.au
- [Queanbeyan-Palerang Regional Council](/doc/source/impactapps_com_au.md) / qprc.nsw.gov.au
- [RecycleSmart](/doc/source/recyclesmart_com.md) / recyclesmart.com
- [Redland City Council (QLD)](/doc/source/redland_qld_gov_au.md) / redland.qld.gov.au
- [Shellharbour City Council](/doc/source/shellharbourwaste_com_au.md) / shellharbourwaste.com.au
- [Singleton Council](/doc/source/impactapps_com_au.md) / singleton.nsw.gov.au
- [Snowy Valleys Council](/doc/source/impactapps_com_au.md) / snowyvalleys.nsw.gov.au
- [South Burnett Regional Council](/doc/source/impactapps_com_au.md) / southburnett.qld.gov.au
- [Stonnington City Council](/doc/source/stonnington_vic_gov_au.md) / stonnington.vic.gov.au
- [The Hawkesbury City Council, Sydney](/doc/source/hawkesbury_nsw_gov_au.md) / hawkesbury.nsw.gov.au
- [The Hills Shire Council, Sydney](/doc/source/thehills_nsw_gov_au.md) / thehills.nsw.gov.au
- [Town of Victoria Park](/doc/source/victoriapark_wa_gov_au.md) / victoriapark.wa.gov.au
- [Townsville](/doc/source/townsville_qld_gov_au.md) / townsville.qld.gov.au
- [Unley City Council (SA)](/doc/source/unley_sa_gov_au.md) / unley.sa.gov.au
- [Wellington Shire Council](/doc/source/impactapps_com_au.md) / wellington.vic.gov.au
- [Whittlesea City Council](/doc/source/whittlesea_vic_gov_au.md) / whittlesea.vic.gov.au/community-support/my-neighbourhood
- [Wollondilly Shire Council](/doc/source/wollondilly_nsw_gov_au.md) / wollondilly.nsw.gov.au
- [Wollongong City Council](/doc/source/wollongongwaste_com_au.md) / wollongongwaste.com

View File

@@ -0,0 +1,356 @@
from datetime import date, timedelta
from typing import List, Optional, TypedDict, Union, cast
import requests
from waste_collection_schedule import Collection # type: ignore[attr-defined]
TITLE = "Impact Apps"
DESCRIPTION = (
"Source for councils using Impact Apps (wasteInfo.com.au) for waste collection."
)
URL = "https://impactapps.com.au"
TEST_CASES = {
"Random Redland Bay": {
"service": "redland",
"suburb": "Redland Bay",
"street_name": "Boundary Street",
"street_number": "1",
},
"Teneriffe Green Beacon": {
"service": "https://brisbane.waste-info.com.au",
"suburb": "Teneriffe",
"street_name": "Helen St",
"street_number": "26",
},
"Test Penrith Address": {
"service": "Penrith City Council",
"property_id": 71794,
},
"Random Penrith Address": {
"service": "Penrith City Council",
"suburb": "Emu Plains",
"street_name": "Beach Street",
"street_number": "3",
},
}
HEADERS = {"user-agent": "Mozilla/5.0"}
ICON_MAP = {
"waste": "mdi:trash-can",
"recycle": "mdi:recycle",
"organic": "mdi:leaf",
}
SERVICE_MAP = [
{
"name": "Baw Baw Shire Council",
"url": "https://baw-baw.waste-info.com.au",
"website": "https://www.bawbawshire.vic.gov.au",
},
{
"name": "Bayside City Council",
"url": "https://bayside.waste-info.com.au",
"website": "https://www.bayside.vic.gov.au",
},
{
"name": "Bega Valley Shire Council",
"url": "https://bega.waste-info.com.au",
"website": "https://www.begavalley.nsw.gov.au",
},
{
"name": "Blue Mountains City Council",
"url": "https://bmcc-waste.waste-info.com.au",
"website": "https://www.bmcc.nsw.gov.au",
},
{
"name": "Burwood City Council",
"url": "https://burwood-waste.waste-info.com.au",
"website": "https://www.burwood.nsw.gov.au",
},
{
"name": "Cowra Council",
"url": "https://cowra.waste-info.com.au",
"website": "https://www.cowracouncil.com.au/",
},
{
"name": "Forbes Shire Council",
"url": "https://forbes.waste-info.com.au",
"website": "https://www.forbes.nsw.gov.au",
},
{
"name": "Gwydir Shire Council",
"url": "https://gwydir.waste-info.com.au",
"website": "https://www.gwydir.nsw.gov.au",
},
{
"name": "Lithgow City Council",
"url": "https://lithgow.waste-info.com.au",
"website": "https://www.lithgow.nsw.gov.au",
},
{
"name": "Livingstone Shire Council",
"url": "https://livingstone.waste-info.com.au",
"website": "https://www.livingstone.qld.gov.au",
},
{
"name": "Loddon Shire Council",
"url": "https://loddon.waste-info.com.au",
"website": "https://www.loddon.vic.gov.au",
},
{
"name": "Moira Shire Council",
"url": "https://moira.waste-info.com.au",
"website": "https://www.moira.vic.gov.au",
},
{
"name": "Moree Plains Shire Council",
"url": "https://moree-waste.waste-info.com.au",
"website": "https://www.mpsc.nsw.gov.au",
},
{
"name": "Penrith City Council",
"url": "https://penrith.waste-info.com.au",
"website": "https://www.penrithcity.nsw.gov.au",
},
{
"name": "Port Macquarie Hastings Council",
"url": "https://pmhc.waste-info.com.au",
"website": "https://www.pmhc.nsw.gov.au",
},
{
"name": "Queanbeyan-Palerang Regional Council",
"url": "https://qprc.waste-info.com.au",
"website": "https://www.qprc.nsw.gov.au",
},
{
"name": "Singleton Council",
"url": "https://singleton.waste-info.com.au",
"website": "https://www.singleton.nsw.gov.au",
},
{
"name": "Snowy Valleys Council",
"url": "https://snowy-valleys.waste-info.com.au",
"website": "https://www.snowyvalleys.nsw.gov.au",
},
{
"name": "South Burnett Regional Council",
"url": "https://sbrc.waste-info.com.au",
"website": "https://www.southburnett.qld.gov.au",
},
{
"name": "Wellington Shire Council",
"url": "https://wellington.waste-info.com.au",
"website": "https://www.wellington.vic.gov.au",
},
]
SERVICE_MAP_LOOKUP = {council["name"]: council for council in SERVICE_MAP}
def EXTRA_INFO():
return [
{
"title": council["name"],
"url": council["website"],
"default_params": {"service": council["name"]},
}
for council in SERVICE_MAP
]
class LocalityResponse(TypedDict):
id: int
name: str
postcode: Optional[int]
council: str
class StreetResponse(TypedDict):
id: int
name: str
locality: str
class PropertyResponse(TypedDict):
id: int
name: str
zone: str
voucher_preferences: int
class OneOffEventResponse(TypedDict):
start: str
event_type: str
color: str
textColor: str
borderColor: str
class RecurringEventResponse(TypedDict):
start_date: str
event_type: str
color: str
textColor: str
borderColor: str
dow: List[int]
daysOfWeek: List[int]
def generate_recurring_dates(
event: RecurringEventResponse, start_date: date, end_date: date
) -> List[date]:
# Generate a list of dates for the recurring event
recurring_dates = []
# Event days of week are indexed with Monday being 1 (1 = Monday, 7 = Sunday)
start_date = date.fromisoformat(event["start_date"])
for i in range((end_date - start_date).days + 1):
current_date = start_date + timedelta(days=i)
if current_date.weekday() + 1 in event["daysOfWeek"]:
recurring_dates.append(current_date)
return recurring_dates
class LocationFinder:
def __init__(self, api_url: str):
self.api_url = api_url
def find_suburb_id(self, session: requests.Session, suburb: str) -> int:
url = f"{self.api_url}/api/v1/localities.json"
response = session.get(url)
response.raise_for_status()
suburbs: List[LocalityResponse] = response.json()["localities"]
suburb_id = next(
(item["id"] for item in suburbs if item["name"] == suburb), None
)
if suburb_id is None:
raise ValueError(f"Suburb {suburb} not found")
return suburb_id
def find_street_id(
self, session: requests.Session, suburb_id: int, street_name: str
) -> int:
url = f"{self.api_url}/api/v1/streets.json"
response = session.get(url, params={"locality": suburb_id})
response.raise_for_status()
streets: List[StreetResponse] = response.json()["streets"]
street_id = next(
(item["id"] for item in streets if item["name"] == street_name), None
)
if street_id is None:
raise ValueError(f"Street {street_name} not found")
return street_id
def find_property_id(
self,
session: requests.Session,
street_id: int,
street_number: str,
street_name: str,
suburb: str,
) -> int:
url = f"{self.api_url}/api/v1/properties.json"
response = session.get(url, params={"street": street_id})
response.raise_for_status()
properties: List[PropertyResponse] = response.json()["properties"]
property_id = next(
(
item["id"]
for item in properties
if item["name"] == f"{street_number} {street_name} {suburb}"
),
None,
)
if property_id is None:
raise ValueError(
f"Property {street_number} {street_name} {suburb} not found"
)
return property_id
class Source:
def __init__(
self,
service: str,
property_id: Optional[int] = None,
suburb: Optional[str] = None,
street_name: Optional[str] = None,
street_number: Optional[str] = None,
):
if service in SERVICE_MAP_LOOKUP:
api_url = SERVICE_MAP_LOOKUP[service]["url"]
else:
if service.startswith("https://"):
api_url = service
else:
# Assume the service is a council name
api_url = f"https://{service}.waste-info.com.au"
self.api_url = api_url
self.property_id = property_id
if not property_id:
if not suburb or not street_name or not street_number:
raise ValueError(
"You must provide a property ID or a suburb, street name and street number"
)
self.suburb = suburb
self.street_name = street_name
self.street_number = street_number
self.location_finder = LocationFinder(self.api_url)
def fetch(self) -> List[Collection]:
start_date = date.today()
end_date = start_date + timedelta(365)
session = requests.Session()
session.headers.update(HEADERS)
if not self.property_id:
suburb_id = self.location_finder.find_suburb_id(session, self.suburb)
street_id = self.location_finder.find_street_id(
session, suburb_id, self.street_name
)
self.property_id = self.location_finder.find_property_id(
session, street_id, self.street_number, self.street_name, self.suburb
)
# Retrieve the collection events for the property
url = f"{self.api_url}/api/v1/properties/{self.property_id}.json"
response = session.get(
url, params={"start": start_date.isoformat(), "end": end_date.isoformat()}
)
events: List[
Union[RecurringEventResponse, OneOffEventResponse]
] = response.json()
collections: List[Collection] = []
for event in events:
event_type = event["event_type"]
icon = ICON_MAP.get(event_type, None)
# Events with a start key are one off events
# Events with a start_date key are recurring events
is_recurring = "start_date" in event
if is_recurring:
recurring_event = cast(RecurringEventResponse, event)
collection_dates = generate_recurring_dates(
recurring_event, start_date, end_date
)
else:
one_off_event = cast(OneOffEventResponse, event)
collection_dates = [date.fromisoformat(one_off_event["start"])]
for collection_date in collection_dates:
collections.append(
Collection(
date=collection_date,
t=event_type,
icon=icon,
)
)
# Order the collections by date
collections = sorted(collections, key=lambda x: x.date)
return collections

View File

@@ -0,0 +1,89 @@
# Impact Apps
Support for schedules provided by [impactapps.com.au](https://impactapps.com.au/). ImpactApps are the creators of the platform used by several councils in Australia to provide waste collection schedules. The API is hosted as a subdomain of `waste-info.com.au` for each council.
Some of the supported councils are available at [calendars.impactapps.com.au](https://calendars.impactapps.com.au/), however, not all councils are listed there.
## Configuration via configuration.yaml
```yaml
waste_collection_schedule:
sources:
- name: impactapps_com_au
args:
service: COUNCIL_NAME or API URL
property_id: PROPERTY_ID
suburb: SUBURB
street_name: STREET_NAME
street_number: STREET_NUMBER
```
### Configuration Variables
**service**
*(string) (required)*
The service can be provided in 1 of 3 different ways:
1. Use one of the following known councils:
- `Baw Baw Shire Council`
- `Bayside City Council`
- `Blue Mountains City Council`
- `Bega Valley Shire Council`
- `Burwood City Council`
- `Cowra Council`
- `Forbes Shire Council`
- `Gwydir Shire Council`
- `Lithgow City Council`
- `Livingstone Shire Council`
- `Loddon Shire Council`
- `Moira Shire Council`
- `Moree Plains Shire Council`
- `Penrith City Council`
- `Port Macquarie Hastings Council`
- `Queanbeyan-Palerang Regional Council`
- `Singleton Council`
- `Snowy Valleys Council`
- `South Burnett Regional Council`
- `Wellington Shire Council`
1. Provide the api url for the council. For example:
- `https://baw-baw.waste-info.com.au`
- `https://bayside.waste-info.com.au`
1. Provide the council component of the url. For example:
- `baw-baw`
- `bayside`
**property_id**
*(integer) (optional)*\
The property id can be found by inspecting the network traffic when using the council's website to find the schedule.
OR
**suburb**
*(string) (optional)*\
It must match the suburb of the address available via the calendar provided on the council's website.
**street_name**
*(string) (optional)*\
It must match the street name of the address available via the calendar provided on the council's website.
**street_number**
*(string) (optional)*\
It must match the street number of the address available via the calendar provided on the council's website.
## Example
```yaml
waste_collection_schedule:
sources:
- name: impactapps_com_au
args:
service: "Penrith City Council"
suburb: "Emu Plains"
street_name: "Beach Street"
street_number: "3"
```

File diff suppressed because one or more lines are too long