Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/abfallio graphql #3788

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@
"url": "https://www.geb-goettingen.de/",
"service_id": "7dd0d724cbbd008f597d18fcb1f474cb",
},
{
"title": "Landkreis Heilbronn",
"url": "https://www.landkreis-heilbronn.de/",
"service_id": "1a1e7b200165683738adddc4bd0199a2",
},
{
"title": "Abfallwirtschaft Landkreis Kitzingen",
"url": "https://www.abfallwelt.de/",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env python3


SERVICE_MAP = [
{
"title": "Landkreis Heilbronn",
"url": "https://www.landkreis-heilbronn.de/",
"service_id": "1a1e7b200165683738adddc4bd0199a2",
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import datetime
import logging
import re
from html.parser import HTMLParser

import requests
from waste_collection_schedule import Collection # type: ignore[attr-defined]
from waste_collection_schedule.service.AbfallIOGraphQL import SERVICE_MAP
from waste_collection_schedule.service.ICS import ICS

TITLE = "Abfall.IO GraphQL"
DESCRIPTION = (
"Source for AbfallPlus.de waste collection. Service is hosted on abfall.io."
)
URL = "https://www.abfallplus.de"
COUNTRY = "de"


def EXTRA_INFO():
return [
{
"title": s["title"],
"url": s["url"],
"default_params": {"key": s["service_id"]},
}
for s in SERVICE_MAP
]


TEST_CASES = {
"Neckarsulm": {
"key": "18adb00cb5135f6aa16b5fdea6dae2c63a507ae0f836540e",
"idHouseNumber": 304,
# "wasteTypes": ["28", "19", "31", "654"]
},
"Weinsberg": {
"key": "18adb00cb5135f6aa16b5fdea6dae2c63a507ae0f836540e",
"idHouseNumber": 353,
# "wasteTypes": ["28", "19", "31", "654"]
},
}
_LOGGER = logging.getLogger(__name__)

MODUS_KEY = "d6c5855a62cf32a4dadbc2831f0f295f"
HEADERS = {"user-agent": "Mozilla/5.0 (xxxx Windows NT 10.0; Win64; x64)"}


# Parser for HTML input (hidden) text
class HiddenInputParser(HTMLParser):
def __init__(self):
super().__init__()
self._args = {}

@property
def args(self):
return self._args

def handle_starttag(self, tag, attrs):
if tag == "input":
d = dict(attrs)
if d["type"] == "hidden":
self._args[d["name"]] = d["value"]


class Source:
def __init__(
self,
key,
idHouseNumber,
wasteTypes=[],
):
self._key = key
self._idHouseNumber = idHouseNumber
self._wasteTypes = wasteTypes # list of integers
self._ics = ICS()

def fetch(self):
# get token
params = {"key": self._key, "modus": MODUS_KEY, "waction": "init"}

now = datetime.datetime.now()
date2 = now + datetime.timedelta(days=365)
args["timeperiod"] = f"{now.strftime('%Y%m%d')}-{date2.strftime('%Y%m%d')}"

body = {
"query": "\nquery Query($idHouseNumber: ID!, $wasteTypes: [ID], $dateMin: Date, $dateMax: Date, $showInactive: Boolean) {\nappointments(idHouseNumber: $idHouseNumber, wasteTypes: $wasteTypes, dateMin: $dateMin, dateMax: $dateMax, showInactive: $showInactive) {\nid\ndate\ntime\nlocation\nnote\nwasteType {\nid\nname\ncolor\ninternals {\npdfLegend\niconLow\n}\n}\n}\n}\n",
"variables": {
"idHouseNumber": self._idHouseNumber,
"wasteTypes": self._wasteTypes,
"dateMin": now.strftime("%Y-%m-%d"),
"dateMax": date2.strftime("%Y-%m-%d"),
}
}

r = requests.post("https://widgets.abfall.io/graphql",
params=params, headers=HEADERS, data=body)

# add all hidden input fields to form data
# There is one hidden field which acts as a token:
# It consists of a UUID key and a UUID value.
p = HiddenInputParser()
p.feed(r.text)
args = p.args

args["idHouseNumber"] = self._idHouseNumber

for i in range(len(self._wasteTypes)):
args[f"f_id_abfalltyp_{i}"] = self._wasteTypes[i]

args["wasteTypes_index_max"] = len(self._wasteTypes)
args["wasteTypes"] = ",".join(
map(lambda x: str(x), self._wasteTypes))

params = {"key": self._key, "modus": MODUS_KEY,
"waction": "export_ics"}

# get csv file
r = requests.post(
"https://api.abfall.io", params=params, data=args, headers=HEADERS
)

# parse ics file
r.encoding = "utf-8" # requests doesn't guess the encoding correctly
ics_file = r.text

# Remove all lines starting with <b
# This warning are caused for customers which use an extra radiobutton
# list to add special waste types:
# - AWB Limburg-Weilheim uses this list to select a "Sonderabfall <city>"
# waste type. The warning could be removed by adding the extra config
# option "f_abfallarten" with the following values [27, 28, 17, 67]
html_warnings = re.findall(r"\<b.*", ics_file)
if html_warnings:
ics_file = re.sub(r"\<br.*|\<b.*", "\\r", ics_file)
# _LOGGER.warning("Html tags removed from ics file: " + ', '.join(html_warnings))

dates = self._ics.convert(ics_file)

entries = []
for d in dates:
entries.append(Collection(d[0], d[1]))
return entries
67 changes: 67 additions & 0 deletions doc/source/abfall_io_graphql.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Abfall.IO /AbfallPlus - GraphQL

Support for schedules provided by [Abfall.IO](https://abfall.io). The official homepage is using the URL [AbfallPlus.de](https://www.abfallplus.de/) instead.

This source is designed for the old API of Abfall.IO. If your region/preovider uses the new API you should use the [Abfall ICS version](/doc/ics/abfall_io_ics.md) source instead. Your provider uses the new API if you can see an `ICS` button above your collection dates on the website after selecting your location and waste types.

## Configuration via configuration.yaml

```yaml
waste_collection_schedule:
sources:
- name: abfall_io
args:
key: KEY
idHouseNumber: HOUSE_NUMBER_ID
wasteTypes:
- 1
- 2
- 3
```

### Configuration Variables

**key**
*(hash) (required)*

**idHouseNumber**
*(integer) (required)*

**wasteTypes**
*(list of integer) (optional)*

## Example

```yaml
waste_collection_schedule:
sources:
- name: abfall_io
args:
key: "8215c62763967916979e0e8566b6172e"
idHouseNumber: 304
```

## How to get the source arguments

## Simple Variant: Use wizard script

There is a script with an interactive command line interface which generates the required source configuration:

[https://github.com/mampfes/hacs_waste_collection_schedule/blob/master/custom_components/waste_collection_schedule/waste_collection_schedule/wizard/abfall_io_graphql.py](https://github.com/mampfes/hacs_waste_collection_schedule/blob/master/custom_components/waste_collection_schedule/waste_collection_schedule/wizard/abfall_io_graphql.py).

First, install the Python module `inquirer`. Then run this script from a shell and answer the questions.

### Hardcore Variant: Extract arguments from website

Another way get the source arguments is to us a (desktop) browser with developer tools, e.g. Google Chrome:

1. Open your county's `Abfuhrtermine` homepage, e.g. [https://www.lrabb.de/start/Service+_+Verwaltung/Abfuhrtermine.html](https://www.lrabb.de/start/Service+_+Verwaltung/Abfuhrtermine.html).
2. Enter your data, but don't click on `Datei exportieren` so far!
3. Select `Exportieren als`: `ICS`
4. Open the Developer Tools (Ctrl + Shift + I) and open the `Network` tab.
5. Now click the `Datei exportieren` button.
6. You should see one entry in the network recording.
7. Select the entry on the left hand side and scroll down to `Query String Parameters` on the right hand side.
8. Here you can find the value for `key`.
9. Now go down to the next section `Form Data`.
10. Here you can find the values for `idHouseNumber`, `wasteTypes`. All other entries don't care.