From 4b8577ef490481dd812a88524e9353a188183125 Mon Sep 17 00:00:00 2001 From: Sander Smeenk Date: Sat, 28 Dec 2024 09:56:36 +0100 Subject: [PATCH] add support for kcm (klikogroep / klikocontainermanager) (#361) * add support for kcm (klikogroep / klikocontainermanager) * allow spaces pre- and post-fixing passwords * suffix not used in this collector * remove extraneous empty line * test is useless here, done in main_collector.py * add user/pass to readme examples * import datetime unnecessary in this collector * collector only uses provider, user and pass args * flake8 / style code improvements --- README.md | 3 + .../afvalwijzer/collector/klikogroep.py | 92 +++++++++++++++++++ .../afvalwijzer/collector/main_collector.py | 13 ++- .../afvalwijzer/common/main_functions.py | 3 +- custom_components/afvalwijzer/config_flow.py | 4 + custom_components/afvalwijzer/const/const.py | 9 ++ custom_components/afvalwijzer/sensor.py | 10 ++ .../afvalwijzer/sensor_provider.py | 4 +- .../afvalwijzer/translations/en.json | 2 + .../afvalwijzer/translations/nl.json | 2 + 10 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 custom_components/afvalwijzer/collector/klikogroep.py diff --git a/README.md b/README.md index b022aee..f271666 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ _Component to integrate with the following providers/communities. Be aware that | rad (ximmio) | | westland (ximmio) | | woerden (ximmio) | +| oudeijsselstreek (klikogroep, needs user / pass) | This custom component dynamically creates sensor.afvalwijzer\_\* items. For me personally the items created are gft, restafval, papier, pmd and kerstbomen. Look in the states overview in the developer tools in Home Assistant what the sensor names for your region are and modify where necessary. @@ -198,6 +199,8 @@ Here's an example of my own Home Asisstant config: https://github.com/xirixiz/ho postal_code: 1234AB # (required, default = '') street_number: 5 # (required, default = '') suffix: '' # (optional, default = '') + username: '' # (optional, default = '') + password: '' # (optional, default = '') exclude_pickup_today: true # (optional, default = true) to take or not to take Today into account in the next pickup. date_isoformat: false # (optional, default = false) show the date in full isoformat if desired. Example: "2024-01-14T08:40:33.993521" default_label: geen # (optional, default = geen) label if no date found diff --git a/custom_components/afvalwijzer/collector/klikogroep.py b/custom_components/afvalwijzer/collector/klikogroep.py new file mode 100644 index 0000000..edfe390 --- /dev/null +++ b/custom_components/afvalwijzer/collector/klikogroep.py @@ -0,0 +1,92 @@ +from ..const.const import _LOGGER, SENSOR_COLLECTORS_KLIKOGROEP +from ..common.main_functions import _waste_type_rename + +import requests +from urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + + +def get_waste_data_raw(provider, username, password): + url = SENSOR_COLLECTORS_KLIKOGROEP[provider]['url'] + app = SENSOR_COLLECTORS_KLIKOGROEP[provider]['app'] + + headers = { + 'Content-Type': 'application/json', + 'Referer': url, + } + + ########################################################################## + # First request: login and get token + ########################################################################## + data = { + "cardNumber": username, + "password": password, + "clientName": provider, + "app": app, + } + + try: + raw_response = requests.post(url="{}/loginWithPassword".format(url), timeout=60, headers=headers, json=data) + raw_response.raise_for_status() + except requests.exceptions.RequestException as err: + raise ValueError(err) from err + + try: + response = raw_response.json() + except ValueError as err: + raise ValueError(f"Invalid and/or no data received from {url}/loginWithPassword") from err + + if 'success' not in response or not response['success']: + _LOGGER.error('Login failed. Check card number (username) and / or password!') + return + + token = response["token"] + + ########################################################################## + # Second request: get the dates + ########################################################################## + data = { + "token": token, + "clientName": provider, + "app": app, + } + + try: + raw_response = requests.post(url="{}/getMyWasteCalendar".format(url), timeout=60, headers=headers, json=data) + raw_response.raise_for_status() + except requests.exceptions.RequestException as err: + raise ValueError(err) from err + + try: + response = raw_response.json() + except ValueError as err: + raise ValueError(f"Invalid and/or no data received from {url}/getMyWasteCalendar") from err + + waste_data_raw = [] + waste_type_mapping = {} + for waste_type in response['fractions']: + waste_type_mapping[waste_type['id']] = _waste_type_rename(waste_type['name'].lower()) + + for pickup_date in response["dates"]: + num_pickup = len(response["dates"][pickup_date][0]) + for idx in range(0, num_pickup): + pick_up = response["dates"][pickup_date][0][idx] + if pick_up != 0: + waste_data_raw.append({ + "type": waste_type_mapping[pick_up], + "date": pickup_date, + }) + + ########################################################################## + # Third request: invalidate token / close session + ########################################################################## + data = { + "token": token, + "clientName": provider, + "app": app, + } + + response = requests.post(url="{}/logout".format(url), timeout=60, headers=headers, json=data).json() + # We really don't care about this result, honestly. + + return waste_data_raw diff --git a/custom_components/afvalwijzer/collector/main_collector.py b/custom_components/afvalwijzer/collector/main_collector.py index 6085bdd..d268534 100644 --- a/custom_components/afvalwijzer/collector/main_collector.py +++ b/custom_components/afvalwijzer/collector/main_collector.py @@ -7,6 +7,7 @@ SENSOR_COLLECTORS_CIRCULUS, SENSOR_COLLECTORS_DEAFVALAPP, SENSOR_COLLECTORS_ICALENDAR, + SENSOR_COLLECTORS_KLIKOGROEP, SENSOR_COLLECTORS_OPZET, SENSOR_COLLECTORS_RD4, SENSOR_COLLECTORS_ROVA, @@ -14,7 +15,7 @@ ) try: - from . import afvalalert, burgerportaal, circulus, deafvalapp, icalendar, mijnafvalwijzer, opzet, rd4, rova, rwm, ximmio + from . import afvalalert, burgerportaal, circulus, deafvalapp, icalendar, klikogroep, mijnafvalwijzer, opzet, rd4, rova, rwm, ximmio except ImportError as err: _LOGGER.error(f"Import error {err.args}") @@ -26,6 +27,8 @@ def __init__( postal_code, street_number, suffix, + username, + password, exclude_pickup_today, date_isoformat, exclude_list, @@ -36,6 +39,8 @@ def __init__( self.postal_code = str(postal_code).strip().upper() self.street_number = str(street_number).strip() self.suffix = str(suffix).strip().lower() + self.username = str(username).strip().lower() + self.password = str(password) # Handle boolean and string parameters correctly self.exclude_pickup_today = str(exclude_pickup_today).lower() if isinstance( @@ -89,6 +94,12 @@ def __init__( self.street_number, self.suffix, ) + elif provider in SENSOR_COLLECTORS_KLIKOGROEP.keys(): + waste_data_raw = klikogroep.get_waste_data_raw( + self.provider, + self.username, + self.password, + ) elif provider in SENSOR_COLLECTORS_OPZET.keys(): waste_data_raw = opzet.get_waste_data_raw( self.provider, diff --git a/custom_components/afvalwijzer/common/main_functions.py b/custom_components/afvalwijzer/common/main_functions.py index 868bb18..7645d67 100644 --- a/custom_components/afvalwijzer/common/main_functions.py +++ b/custom_components/afvalwijzer/common/main_functions.py @@ -11,6 +11,7 @@ def _waste_type_rename(item_name): "gft & etensresten": "gft", "glass": "glas", "gft afval": "gft", + "gft+e": "gft", "green": "gft", "groene container": "gft", "groente": "gft", @@ -26,6 +27,7 @@ def _waste_type_rename(item_name): "packages": "pmd", "pap": "papier", "paper": "papier", + "pbd": "pmd", "pdb": "pmd", "papier en karton": "papier", "papierinzameling": "papier", @@ -35,7 +37,6 @@ def _waste_type_rename(item_name): "plastic, blik & drinkpakken": "pmd", "plastic, blik & drinkpakken arnhem": "pmd", "plastic, blik & drinkpakken overbetuwe": "pmd", - "pmd": "pmd", "pmdrest": "pmd-restafval", "pmd-zak": "pmd", "pruning_waste": "snoeiafval", diff --git a/custom_components/afvalwijzer/config_flow.py b/custom_components/afvalwijzer/config_flow.py index 97480b4..f172190 100644 --- a/custom_components/afvalwijzer/config_flow.py +++ b/custom_components/afvalwijzer/config_flow.py @@ -9,6 +9,8 @@ CONF_POSTAL_CODE, CONF_STREET_NUMBER, CONF_SUFFIX, + CONF_USERNAME, + CONF_PASSWORD, CONF_EXCLUDE_PICKUP_TODAY, CONF_DATE_ISOFORMAT, CONF_DEFAULT_LABEL, @@ -20,6 +22,8 @@ vol.Required(CONF_POSTAL_CODE): cv.string, vol.Required(CONF_STREET_NUMBER): cv.string, vol.Optional(CONF_SUFFIX, default=""): cv.string, + vol.Optional(CONF_USERNAME, default=""): cv.string, + vol.Optional(CONF_PASSWORD, default=""): cv.string, vol.Optional(CONF_EXCLUDE_PICKUP_TODAY, default=True): cv.boolean, vol.Optional(CONF_DATE_ISOFORMAT, default=False): cv.boolean, vol.Optional(CONF_DEFAULT_LABEL, default="geen"): cv.string, diff --git a/custom_components/afvalwijzer/const/const.py b/custom_components/afvalwijzer/const/const.py index 5cc0f26..26c8d58 100644 --- a/custom_components/afvalwijzer/const/const.py +++ b/custom_components/afvalwijzer/const/const.py @@ -59,6 +59,13 @@ "veldhoven": "https://www.veldhoven.nl/afvalkalender/{5}/{1}-{2}.ics", } +SENSOR_COLLECTORS_KLIKOGROEP = { + "oudeijsselstreek": { + "url": "https://cp-oudeijsselstreek.klikocontainermanager.com/MyKliko", + "app": "cp-oudeijsselstreek.kcm.com" + } +} + SENSOR_COLLECTORS_AFVALWIJZER = [ "mijnafvalwijzer", "afvalstoffendienstkalender", @@ -128,6 +135,8 @@ CONF_POSTAL_CODE = "postal_code" CONF_STREET_NUMBER = "street_number" CONF_SUFFIX = "suffix" +CONF_USERNAME = "username" +CONF_PASSWORD = "password" CONF_DATE_FORMAT = "date_format" CONF_EXCLUDE_PICKUP_TODAY = "exclude_pickup_today" CONF_DEFAULT_LABEL = "default_label" diff --git a/custom_components/afvalwijzer/sensor.py b/custom_components/afvalwijzer/sensor.py index 25e5252..2065ddf 100644 --- a/custom_components/afvalwijzer/sensor.py +++ b/custom_components/afvalwijzer/sensor.py @@ -21,6 +21,8 @@ CONF_POSTAL_CODE, CONF_STREET_NUMBER, CONF_SUFFIX, + CONF_USERNAME, + CONF_PASSWORD, SCAN_INTERVAL, ) from .sensor_custom import CustomSensor @@ -33,6 +35,8 @@ vol.Required(CONF_POSTAL_CODE): cv.string, vol.Required(CONF_STREET_NUMBER): cv.string, vol.Optional(CONF_SUFFIX, default=""): cv.string, + vol.Optional(CONF_USERNAME, default=""): cv.string, + vol.Optional(CONF_PASSWORD, default=""): cv.string, vol.Optional(CONF_EXCLUDE_PICKUP_TODAY, default=True): cv.boolean, vol.Optional(CONF_DATE_ISOFORMAT, default=False): cv.boolean, vol.Optional(CONF_EXCLUDE_LIST, default=""): cv.string, @@ -63,6 +67,8 @@ async def _setup_sensors(hass, config, async_add_entities): postal_code = config.get(CONF_POSTAL_CODE) street_number = config.get(CONF_STREET_NUMBER) suffix = config.get(CONF_SUFFIX, "") + username = config.get(CONF_USERNAME, "") + password = config.get(CONF_PASSWORD, "") exclude_pickup_today = config.get(CONF_EXCLUDE_PICKUP_TODAY, True) date_isoformat = config.get(CONF_DATE_ISOFORMAT, False) exclude_list = config.get(CONF_EXCLUDE_LIST, "") @@ -125,6 +131,8 @@ def update(self): postal_code = self.config.get(CONF_POSTAL_CODE) street_number = self.config.get(CONF_STREET_NUMBER) suffix = self.config.get(CONF_SUFFIX) + username = self.config.get(CONF_USERNAME) + password = self.config.get(CONF_PASSWORD) exclude_pickup_today = self.config.get(CONF_EXCLUDE_PICKUP_TODAY) date_isoformat = self.config.get(CONF_DATE_ISOFORMAT) default_label = self.config.get(CONF_DEFAULT_LABEL) @@ -136,6 +144,8 @@ def update(self): postal_code, street_number, suffix, + username, + password, exclude_pickup_today, date_isoformat, exclude_list, diff --git a/custom_components/afvalwijzer/sensor_provider.py b/custom_components/afvalwijzer/sensor_provider.py index f7e6ec0..fdd0ecc 100644 --- a/custom_components/afvalwijzer/sensor_provider.py +++ b/custom_components/afvalwijzer/sensor_provider.py @@ -17,6 +17,8 @@ CONF_POSTAL_CODE, CONF_STREET_NUMBER, CONF_SUFFIX, + CONF_USERNAME, + CONF_PASSWORD, CONF_DATE_ISOFORMAT, SENSOR_ICON, SENSOR_PREFIX, @@ -48,7 +50,7 @@ def __init__(self, hass, waste_type, fetch_data, config): self._state = self._default_label self._icon = SENSOR_ICON self._unique_id = hashlib.sha1( - f"{waste_type}{config.get(CONF_ID)}{config.get(CONF_POSTAL_CODE)}{config.get(CONF_STREET_NUMBER)}{config.get(CONF_SUFFIX, '')}".encode( + f"{waste_type}{config.get(CONF_ID)}{config.get(CONF_POSTAL_CODE)}{config.get(CONF_STREET_NUMBER)}{config.get(CONF_SUFFIX, '')}{config.get(CONF_USERNAME, '')}{config.get(CONF_PASSWORD, '')}".encode( "utf-8" ) ).hexdigest() diff --git a/custom_components/afvalwijzer/translations/en.json b/custom_components/afvalwijzer/translations/en.json index 1cfb78a..a32a594 100644 --- a/custom_components/afvalwijzer/translations/en.json +++ b/custom_components/afvalwijzer/translations/en.json @@ -9,6 +9,8 @@ "postal_code": "Postal code (e.g., 1234AB)", "street_number": "Street number", "suffix": "Address suffix", + "username": "Provider username", + "password": "Provider password", "exclude_pickup_today": "Exclude today's pickup", "date_isoformat": "Use ISO date format", "default_label": "Default label when no data is available", diff --git a/custom_components/afvalwijzer/translations/nl.json b/custom_components/afvalwijzer/translations/nl.json index d6de13a..a4ba1db 100644 --- a/custom_components/afvalwijzer/translations/nl.json +++ b/custom_components/afvalwijzer/translations/nl.json @@ -9,6 +9,8 @@ "postal_code": "Postcode (bijv. 1234AB)", "street_number": "Huisnummer", "suffix": "Huisnummer toevoeging", + "username": "Provider gebruikersnaam", + "password": "Provider wachtwoord", "exclude_pickup_today": "Sluit ophalen van vandaag uit", "date_isoformat": "Gebruik ISO-datumformaat", "default_label": "Standaard label bij geen datum bekend",