diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c943cb9..78b9f06 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ { "context": "..", "dockerFile": "Dockerfile", - "appPort": "8124:8123", + "appPort": "9123:8123", "runArgs": [ "-e", "GIT_EDTIOR='code --wait'" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..af452f8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,45 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[{Dockerfile*,**.yml}] +indent_style = space +indent_size = 2 + +[{**.sh,rootfs/app/**,rootfs/etc/cont-finish.d/**,rootfs/cont-init.d/**,rootfs/etc/fix-attrs.d/**,rootfs/etc/services.d/**}] +indent_style = space +indent_size = 4 + +# 2 space indentation +[*.{js,json,y{a,}ml,html,cwl}] +indent_style = space +indent_size = 2 + +[*.{md,Rmd,rst}] +trim_trailing_whitespace = false +indent_style = space +indent_size = 2 + +# 4 space indentation +[*.{py,java,r,R}] +indent_style = space +indent_size = 4 + +# Docstrings and comments use max_line_length = 79 +[*.py] +max_line_length = 119 + +[docs/**.txt] +max_line_length = 79 + +# Tab indentation (no size specified) +[*.{bat},Makefile] +indent_style = tab diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 67dc70f..dc723d2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,5 @@ - blank_issues_enabled: false contact_links: - - name: ❔ Ask a question in the HASS community + - name: 🙋 Ask a question in the HASS community url: https://community.home-assistant.io/t/garbage-pickup-date-mijnafvalwijzer-nl-custom-component/34631 - about: If you are unsure where to go, then contacting the HASS community is recommended; Just ask! + about: If you are unsure where to go, then contacting the HASS community is recommended; Just ask! diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml index 5c68070..e925739 100644 --- a/.github/ISSUE_TEMPLATE/issue.yml +++ b/.github/ISSUE_TEMPLATE/issue.yml @@ -1,42 +1,51 @@ name: 🚨 Report an issue description: Report an issue. body: - - type: markdown + - type: checkboxes attributes: - value: | - This issue form is for reporting bugs. + label: Support guidelines + description: Please read the support guidelines before proceeding. + options: + - label: I've read the [support guidelines](https://github.com/xirixiz/homeassistant-afvalwijzer/blob/master/.github/SUPPORT.md) + required: true + - type: textarea + attributes: + label: Description + description: Please provide a brief description of the issue in 1-2 sentences. validations: required: true - attributes: - label: Issue - description: >- - Describe the issue you are experiencing here to communicate to the - maintainers. Tell us what you were trying to do and what happened. - Provide a clear and concise description of what the problem is. - - type: input - id: version + - type: textarea + attributes: + label: Expected behaviour + description: Please describe precisely what you'd expect to happen. validations: required: true + + - type: textarea attributes: - label: Which version has the issue? - description: > - Can be found in the About section. - - type: input - attributes: - label: What was the last working version? - description: > - If known, otherwise leave blank. + label: Actual behaviour + description: Please describe precisely what is actually happening. + validations: + required: true + - type: textarea attributes: - label: Anything in the logs that might be useful? - description: For example, error message, or stack traces. - render: txt + label: Version + description: Please provide the HASS version, component version and system information (ARM, Docker, anything usefull). + value: | + * HASS version: + * Component version: + * Useful system info: + validations: + required: true + - type: textarea attributes: - label: Additional information - description: > - If you have any additional information, use the field below. - Please note, you can attach screenshots or screen recordings here, by - dragging and dropping files in the field below. + label: HASS logs + description: Please provide the HASS logs (preferably with debug enabled). + value: | + * logger -> logs -> custom_components.afvalwijzer: debug in you configuration.yaml + validations: + required: true diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..1dab8e3 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,29 @@ +# Support [![](https://isitmaintained.com/badge/resolution/xirixiz/homeassistant-afvalwijzer.svg)](https://isitmaintained.com/project/xirixiz/homeassistant-afvalwijzer) + +## Reporting an issue + +Please do a search in [open issues](https://github.com/xirixiz/homeassistant-afvalwijzer/issues?utf8=%E2%9C%93&q=) to see if the issue or feature request has already been filed. + +If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment. + +:+1: - upvote + +:-1: - downvote + +If you cannot find an existing issue that describes your bug or feature, submit an issue using the guidelines below. + +## Writing good bug reports and feature requests + +File a single issue per problem and feature request. + +* Do not enumerate multiple bugs or feature requests in the same issue. +* Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes. + +The more information you can provide, the more likely someone will be successful reproducing the issue and finding a fix. + +You are now ready to [create a new issue](https://github.com/xirixiz/homeassistant-afvalwijzer/issues/new/choose)! + +## Closure policy + +* Issues that don't have the information requested above (when applicable) will be closed immediately and the poster directed to the support guidelines. +* Issues that go a week without a response from original poster are subject to closure at my discretion. diff --git a/INFO.md b/INFO.md index a466e5c..3410593 100644 --- a/INFO.md +++ b/INFO.md @@ -9,7 +9,7 @@ postal_code: 1234AB # (required, default = '') street_number: 5 # (required, default = '') suffix: '' # (optional, default = '') - include_date_today: false # (optional, default = false) to take or not to take Today into account in the next pickup. + exclude_pickup_today: true # (optional, default = true) to take or not to take Today into account in the next pickup. default_label: Geen # (optional, default = Geen) label if no date found id: '' # (optional, default = '') use if you'd like to have multiple waste pickup locations in HASS exclude_list: '' # (optional, default = '') comma separated list of wast types (case ignored). F.e. "papier, gft" diff --git a/README.md b/README.md index 2080cb5..32b2937 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,30 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) -[![Open Source Love png1](https://badges.frapsoft.com/os/v1/open-source.png)](https://github.com/ellerbrock/open-source-badges/) - -_Component to integrate with [mijnafvalwijzer][mijnafvalwijzer], [afvalstoffendienstkalender][afvalstoffendienstkalender] and [rova][rova]._ +[![Open Source Love png1](https://badges.frapsoft.com/os/v1/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) + +_Component to integrate with the following collectors._ + +| Collector | +| ---------------------------------| +| mijnafvalwijzer | +| afvalstoffendienstkalender (all) | +| rova | +| acv | +| almere | +| areareiniging | +| avalex | +| avri | +| bar | +| hellendoorn | +| meerlanden | +| meppel | +| rad | +| twentemilieu | +| waardlanden | +| westland | +| ximmio | +| reinis | 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. @@ -83,7 +104,7 @@ 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 = '') - include_date_today: false # (optional, default = false) to take or not to take Today into account in the next pickup. + exclude_pickup_today: true # (optional, default = true) to take or not to take Today into account in the next pickup. default_label: Geen # (optional, default = Geen) label if no date found id: '' # (optional, default = '') use if you'd like to have multiple waste pickup locations in HASS exclude_list: '' # (optional, default = '') comma separated list of wast types (case ignored). F.e. "papier, gft" @@ -104,11 +125,11 @@ input_boolean: ###### AUTOMATION ```yaml automation: - - alias: Afval - Herstel notificatie + - alias: Reset waste notification trigger: platform: state entity_id: input_boolean.waste_moved - to: "on" + to: 'on' for: hours: 12 action: @@ -117,24 +138,17 @@ automation: - service: input_boolean.turn_on entity_id: input_boolean.waste_reminder - - alias: Afval - Bevestig notificatie + - alias: Mark waste as moved from notification trigger: - - platform: event - event_type: mobile_app_notification_action - event_data: - action: "MARK_WASTE_MOVED" + platform: event + event_type: ios.notification_action_fired + event_data: + actionName: MARK_WASTE_MOVED action: - service: input_boolean.turn_on entity_id: input_boolean.waste_moved - - service: notify.family - data: - title: 'Afval' - message: 'Afvaltype(n): {{ states.sensor.afvalwijzer_tomorrow_formatted.state }} zijn aan de straat gezet.' - data: - push: - badge: 0 - - alias: Afval - Verzend notificatie - Tomorrow + - alias: Waste has not been moved trigger: platform: time_pattern hours: "/1" @@ -143,30 +157,24 @@ automation: conditions: - condition: state entity_id: input_boolean.waste_moved - state: "off" + state: 'off' - condition: state entity_id: input_boolean.waste_reminder - state: "on" + state: 'on' - condition: time - after: "18:00:00" - before: "23:00:00" + after: '18:00:00' + before: '23:00:00' - condition: template - value_template: "{{ states('sensor.afvalwijzer_tomorrow_formatted') != 'Geen' }}" + value_template: "{{ states('sensor.afvalwijzer_tomorrow') != 'Geen' }}" action: - service: notify.family data: - title: 'Afval' - message: 'Het is vandaag - {{ now().strftime("%d-%m-%Y") }}. Afvaltype(n): {{ states.sensor.afvalwijzer_tomorrow_formatted.state }} wordt opgehaald op: {{ (as_timestamp(now()) + (24*3600)) | timestamp_custom("%d-%m-%Y", True) }}!' + title: "Afval" + message: 'Het is vandaag - {{ now().strftime("%d-%m-%Y") }}. Afvaltype(n): {{ states.sensor.afvalwijzer_tomorrow.state }} wordt opgehaald op: {{ (as_timestamp(now()) + (24*3600)) | timestamp_custom("%d-%m-%Y", True) }}!' data: - actions: - - action: "MARK_WASTE_MOVED" - title: "Afval verwerkt" - activationMode: "background" - authenticationRequired: no - destructive: yes - behavior: "default" push: - badge: 5 + badge: 0 + category: 'afval' ``` *** @@ -175,8 +183,5 @@ automation: [exampleimg2]: afvalwijzer_lovelace.png [buymecoffee]: https://www.buymeacoffee.com/xirixiz [buymecoffeebedge]: https://camo.githubusercontent.com/cd005dca0ef55d7725912ec03a936d3a7c8de5b5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6275792532306d6525323061253230636f666665652d646f6e6174652d79656c6c6f772e737667 -[mijnafvalwijzer]: https://mijnafvalwijzer.nl -[afvalstoffendienstkalender]: https://afvalstoffendienstkalender.nl -[rova]: https://www.rova.nl [customupdater]: https://github.com/custom-components/custom_updater [customupdaterbadge]: https://img.shields.io/badge/custom__updater-true-success.svg diff --git a/custom_components/__init__.py b/custom_components/afvalwijzer/collector/__init__.py similarity index 100% rename from custom_components/__init__.py rename to custom_components/afvalwijzer/collector/__init__.py diff --git a/custom_components/afvalwijzer/collector/mijnafvalwijzer.py b/custom_components/afvalwijzer/collector/mijnafvalwijzer.py new file mode 100644 index 0000000..906b3cc --- /dev/null +++ b/custom_components/afvalwijzer/collector/mijnafvalwijzer.py @@ -0,0 +1,212 @@ +from datetime import datetime + +import requests + +from ..common.day_sensor_data import DaySensorData +from ..common.next_sensor_data import NextSensorData +from ..const.const import ( + _LOGGER, + DATE_TODAY, + DATE_TOMORROW, + SENSOR_COLLECTOR_TO_URL, + SENSOR_COLLECTORS_AFVALWIJZER, +) + + +class MijnAfvalWijzerCollector(object): + def __init__( + self, + provider, + postal_code, + street_number, + suffix, + exclude_pickup_today, + exclude_list, + default_label, + ): + self.provider = provider + self.postal_code = postal_code + self.street_number = street_number + self.suffix = suffix + self.exclude_pickup_today = exclude_pickup_today + self.exclude_list = exclude_list.strip().lower() + self.default_label = default_label + + if self.provider not in SENSOR_COLLECTORS_AFVALWIJZER: + raise ValueError("Invalid provider: %s, please verify", self.provider) + + if self.provider == "rova": + self.provider = "inzamelkalender.rova" + + ( + self._waste_data_raw, + self._waste_data_with_today, + self._waste_data_without_today, + ) = self.get_waste_data_provider() + + ( + self._waste_data_provider, + self._waste_types_provider, + self._waste_data_custom, + self._waste_types_custom, + ) = self.transform_waste_data() + + def get_waste_data_provider(self): + try: + url = SENSOR_COLLECTOR_TO_URL["afvalwijzer_data_default"][0].format( + self.provider, + self.postal_code, + self.street_number, + self.suffix, + datetime.today().strftime("%Y-%m-%d"), + ) + raw_response = requests.get(url) + except requests.exceptions.RequestException as err: + raise ValueError(err) + + try: + json_response = raw_response.json() + except ValueError: + raise ValueError("No JSON data received from " + url) + + try: + waste_data_raw = ( + json_response["ophaaldagen"]["data"] + + json_response["ophaaldagenNext"]["data"] + ) + except ValueError: + raise ValueError("Invalid and/or no JSON data received from " + url) + + try: + waste_data_with_today = {} + waste_data_without_today = {} + + for item in waste_data_raw: + item_date = datetime.strptime(item["date"], "%Y-%m-%d") + item_name = item["type"].strip().lower() + if item_name not in self.exclude_list: + if item_name not in waste_data_with_today: + if item_date >= DATE_TODAY: + waste_data_with_today[item_name] = item_date + + for item in waste_data_raw: + item_date = datetime.strptime(item["date"], "%Y-%m-%d") + item_name = item["type"].strip().lower() + if item_name not in self.exclude_list: + if item_name not in waste_data_without_today: + if item_date > DATE_TODAY: + waste_data_without_today[item_name] = item_date + + try: + for item in waste_data_raw: + item_name = item["type"].strip().lower() + if item_name not in self.exclude_list: + if item_name not in waste_data_with_today.keys(): + waste_data_with_today[item_name] = self.default_label + if item_name not in waste_data_without_today.keys(): + waste_data_without_today[item_name] = self.default_label + except Exception as err: + _LOGGER.error("Other error occurred: %s", err) + + return waste_data_raw, waste_data_with_today, waste_data_without_today + except Exception as err: + _LOGGER.error("Other error occurred: %s", err) + + ########################################################################## + # COMMON CODE + ########################################################################## + def transform_waste_data(self): + if self.exclude_pickup_today.casefold() in ("false", "no"): + date_selected = DATE_TODAY + waste_data_provider = self._waste_data_with_today + else: + date_selected = DATE_TOMORROW + waste_data_provider = self._waste_data_without_today + + try: + waste_types_provider = sorted( + set( + list( + waste["type"] + for waste in self.waste_data_raw + if waste["type"] not in self.exclude_list + ) + ) + ) + except Exception as err: + _LOGGER.error("Other error occurred waste_types_provider: %s", err) + + try: + waste_data_formatted = list( + { + "type": waste["type"], + "date": datetime.strptime(waste["date"], "%Y-%m-%d"), + } + for waste in self.waste_data_raw + if waste["type"] in waste_types_provider + ) + except Exception as err: + _LOGGER.error("Other error occurred waste_data_formatted: %s", err) + + days = DaySensorData(waste_data_formatted, self.default_label) + + try: + waste_data_after_date_selected = list( + filter( + lambda waste: waste["date"] >= date_selected, waste_data_formatted + ) + ) + except Exception as err: + _LOGGER.error( + "Other error occurred waste_data_after_date_selected: %s", err + ) + + next = NextSensorData(waste_data_after_date_selected, self.default_label) + + try: + waste_data_custom = {**next.next_sensor_data, **days.day_sensor_data} + except Exception as err: + _LOGGER.error("Other error occurred waste_data_custom: %s", err) + + try: + waste_types_custom = list(sorted(waste_data_custom.keys())) + except Exception as err: + _LOGGER.error("Other error occurred waste_types_custom: %s", err) + + return ( + waste_data_provider, + waste_types_provider, + waste_data_custom, + waste_types_custom, + ) + + ########################################################################## + # PROPERTIES FOR EXECUTION + ########################################################################## + @property + def waste_data_raw(self): + return self._waste_data_raw + + @property + def waste_data_with_today(self): + return self._waste_data_with_today + + @property + def waste_data_without_today(self): + return self._waste_data_without_today + + @property + def waste_data_provider(self): + return self._waste_data_provider + + @property + def waste_types_provider(self): + return self._waste_types_provider + + @property + def waste_data_custom(self): + return self._waste_data_custom + + @property + def waste_types_custom(self): + return self._waste_types_custom diff --git a/custom_components/afvalwijzer/collector/ximmio.py b/custom_components/afvalwijzer/collector/ximmio.py new file mode 100644 index 0000000..6fd01e9 --- /dev/null +++ b/custom_components/afvalwijzer/collector/ximmio.py @@ -0,0 +1,277 @@ +from datetime import datetime + +import requests + +from ..common.day_sensor_data import DaySensorData +from ..common.next_sensor_data import NextSensorData +from ..const.const import ( + _LOGGER, + DATE_TODAY, + DATE_TODAY_NEXT_YEAR, + DATE_TOMORROW, + SENSOR_COLLECTOR_TO_URL, + SENSOR_COLLECTORS_XIMMIO, +) + + +class XimmioCollector(object): + def __init__( + self, + provider, + postal_code, + street_number, + suffix, + exclude_pickup_today, + exclude_list, + default_label, + ): + self.provider = provider + self.postal_code = postal_code + self.street_number = street_number + self.suffix = suffix + self.exclude_pickup_today = exclude_pickup_today + self.exclude_list = exclude_list.strip().lower() + self.default_label = default_label + + if self.provider not in SENSOR_COLLECTORS_XIMMIO.keys(): + raise ValueError("Invalid provider: %s, please verify", self.provider) + + collectors = ("avalex", "meerlanden", "rad", "westland") + if self.provider in collectors: + self.provider_url = "ximmio02" + else: + self.provider_url = "ximmio01" + + ( + self._waste_data_raw, + self._waste_data_with_today, + self._waste_data_without_today, + ) = self.get_waste_data_provider() + + ( + self._waste_data_provider, + self._waste_types_provider, + self._waste_data_custom, + self._waste_types_custom, + ) = self.transform_waste_data() + + def get_waste_data_provider(self): + ########################################################################## + # First request: get uniqueId and community + ########################################################################## + try: + url = SENSOR_COLLECTOR_TO_URL[self.provider_url][0] + companyCode = SENSOR_COLLECTORS_XIMMIO[self.provider] + data = { + "postCode": self.postal_code, + "houseNumber": self.street_number, + "companyCode": companyCode, + } + + raw_response = requests.post(url=url, data=data) + + uniqueId = raw_response.json()["dataList"][0]["UniqueId"] + community = raw_response.json()["dataList"][0]["Community"] + + except requests.exceptions.RequestException as err: + raise ValueError(err) + + ########################################################################## + # Second request: get the dates + ########################################################################## + try: + url = SENSOR_COLLECTOR_TO_URL[self.provider_url][1] + data = { + "companyCode": companyCode, + "startDate": DATE_TODAY.date(), + "endDate": DATE_TODAY_NEXT_YEAR, + "community": community, + "uniqueAddressID": uniqueId, + } + json_response = requests.post(url=url, data=data).json() + + except ValueError: + raise ValueError("No JSON data received from " + url) + + try: + waste_data_raw = json_response["dataList"] + except ValueError: + raise ValueError("Invalid and/or no JSON data received from " + url) + + try: + waste_data_with_today = {} + waste_data_without_today = {} + waste_data_raw_formatted = [] + + for item in waste_data_raw: + temp = {} + temp["type"] = self.__waste_type_rename( + item["_pickupTypeText"].strip().lower() + ) + temp["date"] = datetime.strptime( + item["pickupDates"][0], "%Y-%m-%dT%H:%M:%S" + ).strftime("%Y-%m-%d") + waste_data_raw_formatted.append(temp) + + for item in waste_data_raw_formatted: + item_date = datetime.strptime(item["date"], "%Y-%m-%d") + item_name = item["type"] + if item_name not in self.exclude_list: + if item_name not in waste_data_with_today: + if item_date >= DATE_TODAY: + waste_data_with_today[item_name] = item_date + + for item in waste_data_raw_formatted: + item_date = datetime.strptime(item["date"], "%Y-%m-%d") + item_name = item["type"] + if item_name not in self.exclude_list: + if item_name not in waste_data_without_today: + if item_date > DATE_TODAY: + waste_data_without_today[item_name] = item_date + + try: + for item in waste_data_raw_formatted: + item_name = item["type"] + if item_name not in self.exclude_list: + if item_name not in waste_data_with_today.keys(): + waste_data_with_today[item_name] = self.default_label + if item_name not in waste_data_without_today.keys(): + waste_data_without_today[item_name] = self.default_label + except Exception as err: + _LOGGER.error("Other error occurred: %s", err) + + return ( + waste_data_raw_formatted, + waste_data_with_today, + waste_data_without_today, + ) + except Exception as err: + _LOGGER.error("Other error occurred: %s", err) + + def __waste_type_rename(self, item_name): + if item_name == "branches": + item_name = "takken" + if item_name == "bulklitter": + item_name = "grofvuil" + if item_name == "bulkygardenwaste": + item_name = "tuinafval" + if item_name == "glass": + item_name = "glas" + if item_name == "green": + item_name = "gft" + if item_name == "grey": + item_name = "restafval" + if item_name == "kca": + item_name = "chemisch" + if item_name == "plastic": + item_name = "plastic" + if item_name == "packages": + item_name = "pmd" + if item_name == "paper": + item_name = "papier" + if item_name == "remainder": + item_name = "restwagen" + if item_name == "textile": + item_name = "textiel" + if item_name == "tree": + item_name = "kerstboom" + return item_name + + ########################################################################## + # COMMON CODE + ########################################################################## + def transform_waste_data(self): + if self.exclude_pickup_today.casefold() in ("false", "no"): + date_selected = DATE_TODAY + waste_data_provider = self._waste_data_with_today + else: + date_selected = DATE_TOMORROW + waste_data_provider = self._waste_data_without_today + + try: + waste_types_provider = sorted( + set( + list( + waste["type"] + for waste in self.waste_data_raw + if waste["type"] not in self.exclude_list + ) + ) + ) + except Exception as err: + _LOGGER.error("Other error occurred waste_types_provider: %s", err) + + try: + waste_data_formatted = list( + { + "type": waste["type"], + "date": datetime.strptime(waste["date"], "%Y-%m-%d"), + } + for waste in self.waste_data_raw + if waste["type"] in waste_types_provider + ) + except Exception as err: + _LOGGER.error("Other error occurred waste_data_formatted: %s", err) + + days = DaySensorData(waste_data_formatted, self.default_label) + + try: + waste_data_after_date_selected = list( + filter( + lambda waste: waste["date"] >= date_selected, waste_data_formatted + ) + ) + except Exception as err: + _LOGGER.error( + "Other error occurred waste_data_after_date_selected: %s", err + ) + + next = NextSensorData(waste_data_after_date_selected, self.default_label) + + try: + waste_data_custom = {**next.next_sensor_data, **days.day_sensor_data} + except Exception as err: + _LOGGER.error("Other error occurred waste_data_custom: %s", err) + + try: + waste_types_custom = list(sorted(waste_data_custom.keys())) + except Exception as err: + _LOGGER.error("Other error occurred waste_types_custom: %s", err) + + return ( + waste_data_provider, + waste_types_provider, + waste_data_custom, + waste_types_custom, + ) + + ########################################################################## + # PROPERTIES FOR EXECUTION + ########################################################################## + @property + def waste_data_raw(self): + return self._waste_data_raw + + @property + def waste_data_with_today(self): + return self._waste_data_with_today + + @property + def waste_data_without_today(self): + return self._waste_data_without_today + + @property + def waste_data_provider(self): + return self._waste_data_provider + + @property + def waste_types_provider(self): + return self._waste_types_provider + + @property + def waste_data_custom(self): + return self._waste_data_custom + + @property + def waste_types_custom(self): + return self._waste_types_custom diff --git a/custom_components/afvalwijzer/common/__init__.py b/custom_components/afvalwijzer/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/afvalwijzer/common/day_sensor_data.py b/custom_components/afvalwijzer/common/day_sensor_data.py new file mode 100644 index 0000000..c6ee48a --- /dev/null +++ b/custom_components/afvalwijzer/common/day_sensor_data.py @@ -0,0 +1,58 @@ +from ..const.const import _LOGGER, DATE_DOT, DATE_TODAY, DATE_TOMORROW + + +class DaySensorData(object): + + ########################################################################## + # INIT + ########################################################################## + def __init__( + self, + waste_data_formatted, + default_label, + ): + self.waste_data_formatted = waste_data_formatted + self.today_date = DATE_TODAY + self.tomorrow_date = DATE_TOMORROW + self.day_after_tomorrow_date = DATE_DOT + self.default_label = default_label + + self.waste_data_today = self.__gen_day_sensor(self.today_date) + self.waste_data_tomorrow = self.__gen_day_sensor(self.tomorrow_date) + self.waste_data_dot = self.__gen_day_sensor(self.day_after_tomorrow_date) + + self.data = self._gen_day_sensor_data() + + ########################################################################## + # CREATE TODAY, TOMORROW, DOT SENSOR(S) + ########################################################################## + + # Generate sensor data per date + def __gen_day_sensor(self, date): + day = list() + try: + for waste in self.waste_data_formatted: + item_date = waste["date"] + item_name = waste["type"] + if item_date == date: + day.append(item_name) + if not day: + day.append(self.default_label) + except Exception as err: + _LOGGER.error("Other error occurred __gen_day_sensor: %s", err) + return day + + # Generate sensor data for today, tomorrow, day after tomorrow + def _gen_day_sensor_data(self): + day_sensor = dict() + try: + day_sensor["today"] = ", ".join(self.waste_data_today) + day_sensor["tomorrow"] = ", ".join(self.waste_data_tomorrow) + day_sensor["day_after_tomorrow"] = ", ".join(self.waste_data_dot) + except Exception as err: + _LOGGER.error("Other error occurred _gen_day_sensor_data: %s", err) + return day_sensor + + @property + def day_sensor_data(self): + return self.data diff --git a/custom_components/afvalwijzer/common/next_sensor_data.py b/custom_components/afvalwijzer/common/next_sensor_data.py new file mode 100644 index 0000000..796ab7b --- /dev/null +++ b/custom_components/afvalwijzer/common/next_sensor_data.py @@ -0,0 +1,70 @@ +from ..const.const import _LOGGER, DATE_TODAY + + +class NextSensorData(object): + + ########################################################################## + # INIT + ########################################################################## + def __init__(self, waste_data_after_date_selected, default_label): + self.waste_data_after_date_selected = waste_data_after_date_selected + self.today_date = DATE_TODAY + self.default_label = default_label + + self.next_waste_date = self.__get_next_waste_date() + self.next_waste_in_days = self.__get_next_waste_in_days() + self.next_waste_type = self.__get_next_waste_type() + + self.data = self._gen_next_sensor_data() + + ########################################################################## + # CREATE NEXT SENSOR(S) + ########################################################################## + + # Generate sensor next_waste_date + def __get_next_waste_date(self): + next_waste_date = self.default_label + try: + next_waste_date = self.waste_data_after_date_selected[0]["date"] + except Exception as err: + _LOGGER.error("Other error occurred _get_next_waste_date: %s", err) + return next_waste_date + + # Generate sensor next_waste_in_days + def __get_next_waste_in_days(self): + next_waste_in_days = self.default_label + try: + next_waste_in_days = abs(self.today_date - self.next_waste_date).days + except Exception as err: + _LOGGER.error("Other error occurred _get_next_waste_in_days: %s", err) + return next_waste_in_days + + # Generate sensor next_waste_type + def __get_next_waste_type(self): + next_waste_type = list() + try: + for waste in self.waste_data_after_date_selected: + item_date = waste["date"] + item_name = waste["type"] + if item_date == self.next_waste_date: + next_waste_type.append(item_name) + if not next_waste_type: + next_waste_type.append(self.default_label) + except Exception as err: + _LOGGER.error("Other error occurred _get_next_waste_type: %s", err) + return next_waste_type + + # Generate sensor data for custom sensors + def _gen_next_sensor_data(self): + next_sensor = dict() + try: + next_sensor["next_date"] = self.next_waste_date + next_sensor["next_in_days"] = self.next_waste_in_days + next_sensor["next_type"] = ", ".join(self.next_waste_type) + except Exception as err: + _LOGGER.error("Other error occurred _gen_next_sensor_data: %s", err) + return next_sensor + + @property + def next_sensor_data(self): + return self.data diff --git a/custom_components/afvalwijzer/const/__init__.py b/custom_components/afvalwijzer/const/__init__.py old mode 100755 new mode 100644 diff --git a/custom_components/afvalwijzer/const/const.py b/custom_components/afvalwijzer/const/const.py index bac4fd6..7de4c54 100755 --- a/custom_components/afvalwijzer/const/const.py +++ b/custom_components/afvalwijzer/const/const.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta import logging API = "api" @@ -6,20 +6,60 @@ VERSION = "2021.12.01" ISSUE_URL = "https://github.com/xirixiz/homeassistant-afvalwijzer/issues" -SENSOR_PROVIDER_TO_URL = { +_LOGGER = logging.getLogger(__name__) + +SENSOR_COLLECTOR_TO_URL = { "afvalwijzer_data_default": [ "https://api.{0}.nl/webservices/appsinput/?apikey=5ef443e778f41c4f75c69459eea6e6ae0c2d92de729aa0fc61653815fbd6a8ca&method=postcodecheck&postcode={1}&street=&huisnummer={2}&toevoeging={3}&app_name=afvalwijzer&platform=phone&afvaldata={4}&langs=nl&" ], + "afvalstoffendienstkalender": [ + "https://{0}.afvalstoffendienstkalender.nl/nl/{1}/{2}/" + ], + "afvalstoffendienstkalender-s-hertogenbosch": [ + "https://afvalstoffendienstkalender.nl/nl/{0}/{1}/" + ], + "ximmio01": [ + "https://wasteapi.ximmio.com/api/FetchAdress", + "https://wasteapi.ximmio.com/api/GetCalendar", + ], + "ximmio02": [ + "https://wasteprod2api.ximmio.com/api/FetchAdress", + "https://wasteprod2api.ximmio.com/api/GetCalendar", + ], } -CONF_PROVIDER = "provider" +SENSOR_COLLECTORS_AFVALWIJZER = [ + "mijnafvalwijzer", + "afvalstoffendienstkalender", + "afvalstoffendienstkalender-s-hertogenbosch", + "rova", +] + +SENSOR_COLLECTORS_XIMMIO = { + "acv": "f8e2844a-095e-48f9-9f98-71fceb51d2c3", + "almere": "53d8db94-7945-42fd-9742-9bbc71dbe4c1", + "areareiniging": "adc418da-d19b-11e5-ab30-625662870761", + "avalex": "f7a74ad1-fdbf-4a43-9f91-44644f4d4222", + "avri": "78cd4156-394b-413d-8936-d407e334559a", + "bar": "bb58e633-de14-4b2a-9941-5bc419f1c4b0", + "hellendoorn": "24434f5b-7244-412b-9306-3a2bd1e22bc1", + "meerlanden": "800bf8d7-6dd1-4490-ba9d-b419d6dc8a45", + "meppel": "b7a594c7-2490-4413-88f9-94749a3ec62a", + "rad": "13a2cad9-36d0-4b01-b877-efcb421a864d", + "twentemilieu": "8d97bb56-5afd-4cbc-a651-b4f7314264b4", + "waardlanden": "942abcf6-3775-400d-ae5d-7380d728b23c", + "westland": "6fc75608-126a-4a50-9241-a002ce8c8a6c", + "ximmio": "800bf8d7-6dd1-4490-ba9d-b419d6dc8a45", + "reinis": "9dc25c8a-175a-4a41-b7a1-83f237a80b77", +} + +CONF_COLLECTOR = "provider" CONF_API_TOKEN = "api_token" -# 5ef443e778f41c4f75c69459eea6e6ae0c2d92de729aa0fc61653815fbd6a8ca CONF_POSTAL_CODE = "postal_code" CONF_STREET_NUMBER = "street_number" CONF_SUFFIX = "suffix" CONF_DATE_FORMAT = "date_format" -CONF_INCLUDE_DATE_TODAY = "include_date_today" +CONF_EXCLUDE_PICKUP_TODAY = "exclude_pickup_today" CONF_DEFAULT_LABEL = "default_label" CONF_ID = "id" CONF_EXCLUDE_LIST = "exclude_list" @@ -34,20 +74,24 @@ ATTR_DAYS_UNTIL_COLLECTION_DATE = "days_until_collection_date" ATTR_YEAR_MONTH_DAY_DATE = "year_month_day_date" -_LOGGER = logging.getLogger(__name__) - MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) PARALLEL_UPDATES = 1 SCAN_INTERVAL = timedelta(seconds=30) +TODAY = datetime.today().strftime("%d-%m-%Y") +DATE_TODAY = datetime.strptime(TODAY, "%d-%m-%Y") +DATE_TOMORROW = datetime.strptime(TODAY, "%d-%m-%Y") + timedelta(days=1) +DATE_DOT = datetime.strptime(TODAY, "%d-%m-%Y") + timedelta(days=2) +DATE_TODAY_NEXT_YEAR = (DATE_TODAY.date() + timedelta(days=365)).strftime("%Y-%m-%d") + DOMAIN = "afvalwijzer" DOMAIN_DATA = "afvalwijzer_data" STARTUP_MESSAGE = f""" -------------------------------------------------------------------- -Afvalwijzer -This is a custom integration! -If you have any issues with this you need to open an issue here: -https://github.com/xirixiz/homeassistant-afvalwijzer/issues -------------------------------------------------------------------- +-------------------------------------------------------------------, +Afvalwijzer - {VERSION}, +This is a custom integration!, +If you have any issues with this you need to open an issue here:, +https://github.com/xirixiz/homeassistant-afvalwijzer/issues, +-------------------------------------------------------------------, """ diff --git a/custom_components/afvalwijzer/manifest.json b/custom_components/afvalwijzer/manifest.json index 37cd000..5d6da3b 100644 --- a/custom_components/afvalwijzer/manifest.json +++ b/custom_components/afvalwijzer/manifest.json @@ -1,7 +1,7 @@ { "domain": "afvalwijzer", "name": "Afvalwijzer", - "version": "2021.12.01", + "version": "2022.01.01", "iot_class": "cloud_polling", "documentation": "https://github.com/xirixiz/homeassistant-afvalwijzer/blob/master/README.md", "issue_tracker": "https://github.com/xirixiz/homeassistant-afvalwijzer/issues", @@ -11,4 +11,4 @@ "@xirixiz" ], "requirements": [] -} \ No newline at end of file +} diff --git a/custom_components/afvalwijzer/provider/afvalwijzer.py b/custom_components/afvalwijzer/provider/afvalwijzer.py deleted file mode 100755 index cd0680c..0000000 --- a/custom_components/afvalwijzer/provider/afvalwijzer.py +++ /dev/null @@ -1,336 +0,0 @@ -from datetime import datetime, timedelta -import json - -import requests - -from ..const.const import _LOGGER, SENSOR_PROVIDER_TO_URL - - -class AfvalWijzer(object): - - ########################################################################## - # INIT - ########################################################################## - def __init__( - self, - provider, - postal_code, - street_number, - suffix, - include_date_today, - default_label, - exclude_list, - ): - self.provider = provider - self.postal_code = postal_code - self.street_number = street_number - self.suffix = suffix - self.include_date_today = include_date_today - self.default_label = default_label - self.exclude_list = exclude_list.strip().lower() - - _providers = ( - "mijnafvalwijzer", - "afvalstoffendienstkalender", - "rova", - ) - if self.provider not in _providers: - print("Invalid provider: %s, please verify", self.provider) - - if self.provider == "rova": - self.provider = "inzamelkalender.rova" - - ########################################################################## - # DATE CALCULATION TODAY, TOMORROW, DAY AFTER TOMORROW - ########################################################################## - - # today - self.today = datetime.today().strftime("%d-%m-%Y") - self.today_date = datetime.strptime(self.today, "%d-%m-%Y") - - # tomorow - self.today_to_tomorrow = datetime.strptime(self.today, "%d-%m-%Y") + timedelta( - days=1 - ) - self.tomorrow = datetime.strftime(self.today_to_tomorrow, "%d-%m-%Y") - self.tomorrow_date = datetime.strptime(self.tomorrow, "%d-%m-%Y") - - # day after tomorow - self.today_to_day_after_tomorrow = datetime.strptime( - self.today, "%d-%m-%Y" - ) + timedelta(days=2) - self.day_after_tomorrow = datetime.strftime( - self.today_to_day_after_tomorrow, "%d-%m-%Y" - ) - self.day_after_tomorrow_date = datetime.strptime( - self.day_after_tomorrow, "%d-%m-%Y" - ) - - # data collect - ( - self._waste_data_raw, - self._waste_data_with_today, - self._waste_data_without_today, - ) = self.get_waste_data_provider() - self._waste_data_custom = self.get_waste_data_custom() - self._waste_types_provider = self.get_waste_types_provider() - self._waste_types_custom = self.get_waste_types_custom() - - ########################################################################## - # GET WASTE DATA FROM PROVIDER - ########################################################################## - - def get_waste_data_provider(self): - try: - _LOGGER.debug( - "Connecting to the frontend (json data) of: %s", self.provider - ) - - url = SENSOR_PROVIDER_TO_URL["afvalwijzer_data_default"][0].format( - self.provider, - self.postal_code, - self.street_number, - self.suffix, - datetime.today().strftime("%Y-%m-%d"), - ) - - _LOGGER.debug("URL parsed: %s", url) - - try: - raw_response = requests.get(url) - except requests.exceptions.RequestException as err: - raise ValueError(err) - - try: - json_response = raw_response.json() - except ValueError: - raise ValueError("No JSON data received from " + url) - - try: - waste_data_raw = ( - json_response["ophaaldagen"]["data"] - + json_response["ophaaldagenNext"]["data"] - ) - except ValueError: - raise ValueError("Invalid and/or no JSON data received from " + url) - - if not waste_data_raw: - _LOGGER.error("No waste data found!") - return - - waste_data_with_today = {} - waste_data_without_today = {} - - for item in waste_data_raw: - item_date = datetime.strptime(item["date"], "%Y-%m-%d") - item_name = item["type"].strip().lower() - if item_name not in self.exclude_list: - if item_name not in waste_data_with_today: - if item_date >= self.today_date: - waste_data_with_today[item_name] = item_date - - for item in waste_data_raw: - item_date = datetime.strptime(item["date"], "%Y-%m-%d") - item_name = item["type"].strip().lower() - if item_name not in self.exclude_list: - if item_name not in waste_data_without_today: - if item_date > self.today_date: - waste_data_without_today[item_name] = item_date - - try: - for item in waste_data_raw: - item_name = item["type"].strip().lower() - if item_name not in self.exclude_list: - if item_name not in waste_data_with_today.keys(): - waste_data_with_today[item_name] = self.default_label - if item_name not in waste_data_without_today.keys(): - waste_data_without_today[item_name] = self.default_label - except Exception as err: - _LOGGER.error("Other error occurred: %s", err) - - return waste_data_raw, waste_data_with_today, waste_data_without_today - - except Exception as err: - _LOGGER.error("Other error occurred: %s", err) - - ########################################################################## - # GENERATE DATA FOR CUSTOM SENSORS - ########################################################################## - def get_waste_data_custom(self): - - # start counting wiht Today's date or with Tomorrow"s date - if self.include_date_today.casefold() in ("true", "yes"): - date_selected = self.today_date - else: - date_selected = self.tomorrow_date - - waste_data_custom = {} - today_multiple_items = [] - tomorrow_multiple_items = [] - day_after_tomorrow_multiple_items = [] - next_item_multiple_items = [] - - ########################################################################## - # GENERATE TODAY, TOMORROW, DAY AFTER TOMORROW SENSOR DATA - ########################################################################## - try: - waste_data_provider = self._waste_data_with_today - waste_data_temp = { - key: value - for key, value in waste_data_provider.items() - if isinstance(value, datetime) - } - - for key, value in waste_data_temp.items(): - # waste type(s) today - if value == self.today_date: - if "today" in waste_data_custom.keys(): - today_multiple_items.append(key) - waste_data_custom["today"] = ", ".join(today_multiple_items) - else: - today_multiple_items.append(key) - waste_data_custom["today"] = key - - # waste type(s) tomorrow - if value == self.tomorrow_date: - if "tomorrow" in waste_data_custom.keys(): - tomorrow_multiple_items.append(key) - waste_data_custom["tomorrow"] = ", ".join( - tomorrow_multiple_items - ) - else: - tomorrow_multiple_items.append(key) - waste_data_custom["tomorrow"] = key - - # waste type(s) day_after_tomorrow - if value == self.day_after_tomorrow_date: - if "day_after_tomorrow" in waste_data_custom.keys(): - day_after_tomorrow_multiple_items.append(key) - waste_data_custom["day_after_tomorrow"] = ", ".join( - day_after_tomorrow_multiple_items - ) - else: - day_after_tomorrow_multiple_items.append(key) - waste_data_custom["day_after_tomorrow"] = key - - # set value to none if no value has been found - if "today" not in waste_data_custom.keys(): - waste_data_custom["today"] = self.default_label - if "tomorrow" not in waste_data_custom.keys(): - waste_data_custom["tomorrow"] = self.default_label - if "day_after_tomorrow" not in waste_data_custom.keys(): - waste_data_custom["day_after_tomorrow"] = self.default_label - - except Exception as err: - _LOGGER.error("Error occurred: %s", err) - - ########################################################################## - # GENERATE NEXT_ SENSOR DATA - ########################################################################## - - try: - waste_data_provider = self._waste_data_raw - waste_data_provider_past_removed = list( - filter( - lambda item: datetime.strptime(item["date"], "%Y-%m-%d") - >= date_selected, - waste_data_provider, - ) - ) - - for item in range(len(waste_data_provider_past_removed)): - real_item = len(waste_data_provider_past_removed) - item - 1 - if ( - waste_data_provider_past_removed[real_item]["date"] - == self.default_label - ): - del waste_data_provider_past_removed[real_item] - - for item in range(len(waste_data_provider_past_removed)): - real_item = len(waste_data_provider_past_removed) - item - 1 - if ( - waste_data_provider_past_removed[real_item]["type"] - in self.exclude_list - ): - del waste_data_provider_past_removed[real_item] - - waste_data_provider_next_date = datetime.strptime( - waste_data_provider_past_removed[0]["date"], "%Y-%m-%d" - ) - - for item in waste_data_provider_past_removed: - item_date = datetime.strptime(item["date"], "%Y-%m-%d") - item_name = item["type"].strip().lower() - if item_date == waste_data_provider_next_date: - if "next_item" in waste_data_custom.keys(): - if item_name not in waste_data_custom.keys(): - next_item_multiple_items.append(item_name) - waste_data_custom["next_item"] = ", ".join( - next_item_multiple_items - ) - else: - next_item_multiple_items.append(item_name) - waste_data_custom["next_item"] = item_name - - # first upcoming pickup date of any waste type - waste_data_custom["next_date"] = waste_data_provider_next_date - - # first upcoming waste type pickup in days - waste_data_custom["next_in_days"] = abs( - (self.today_date - waste_data_provider_next_date).days - ) - - # set value to none if no value has been found - if "next_date" not in waste_data_custom.keys(): - waste_data_custom["next_date"] = self.default_label - if "next_in_days" not in waste_data_custom.keys(): - waste_data_custom["next_in_days"] = self.default_label - if "next_item" not in waste_data_custom.keys(): - waste_data_custom["next_item"] = self.default_label - - except Exception as err: - _LOGGER.error("Error occurred: %s", err) - - return waste_data_custom - - ########################################################################## - # GENERATE WASTE TYPES LIST FROM PROVIDER - ########################################################################## - - def get_waste_types_provider(self): - waste_data_provider = self._waste_data_without_today - waste_list_provider = list(waste_data_provider.keys()) - return waste_list_provider - - ########################################################################## - # GENERATE SENSOR TYPES LIST FOR CUSTOM SENSORS - ########################################################################## - - def get_waste_types_custom(self): - waste_data_custom = self._waste_data_custom - waste_list_custom = list(waste_data_custom.keys()) - return waste_list_custom - - ########################################################################## - # PROPERTIES FOR EXECUTION - ########################################################################## - - @property - def waste_data_with_today(self): - return self._waste_data_with_today - - @property - def waste_data_without_today(self): - return self._waste_data_without_today - - @property - def waste_data_custom(self): - return self._waste_data_custom - - @property - def waste_types_provider(self): - return self._waste_types_provider - - @property - def waste_types_custom(self): - return self._waste_types_custom diff --git a/custom_components/afvalwijzer/sensor.py b/custom_components/afvalwijzer/sensor.py index a575311..5cae602 100755 --- a/custom_components/afvalwijzer/sensor.py +++ b/custom_components/afvalwijzer/sensor.py @@ -8,42 +8,43 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from requests.exceptions import HTTPError import voluptuous as vol +from .collector.mijnafvalwijzer import MijnAfvalWijzerCollector +from .collector.ximmio import XimmioCollector from .const.const import ( _LOGGER, + CONF_COLLECTOR, CONF_DEFAULT_LABEL, CONF_EXCLUDE_LIST, + CONF_EXCLUDE_PICKUP_TODAY, CONF_ID, - CONF_INCLUDE_DATE_TODAY, CONF_POSTAL_CODE, - CONF_PROVIDER, CONF_STREET_NUMBER, CONF_SUFFIX, MIN_TIME_BETWEEN_UPDATES, PARALLEL_UPDATES, SCAN_INTERVAL, + SENSOR_COLLECTORS_AFVALWIJZER, + SENSOR_COLLECTORS_XIMMIO, STARTUP_MESSAGE, ) -from .provider.afvalwijzer import AfvalWijzer -from .sensor_custom import AfvalwijzerCustomSensor -from .sensor_provider import AfvalwijzerProviderSensor +from .sensor_custom import CustomSensor +from .sensor_provider import ProviderSensor PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional( - CONF_PROVIDER.strip().lower(), default="mijnafvalwijzer" + CONF_COLLECTOR.strip().lower(), default="mijnafvalwijzer" ): cv.string, vol.Required(CONF_POSTAL_CODE.strip(), default="1234AB"): cv.string, vol.Required(CONF_STREET_NUMBER.strip(), default="5"): cv.string, vol.Optional(CONF_SUFFIX.strip(), default=""): cv.string, - vol.Optional(CONF_INCLUDE_DATE_TODAY.strip(), default="false"): cv.string, + vol.Optional(CONF_EXCLUDE_PICKUP_TODAY.strip(), default="false"): cv.string, + vol.Optional(CONF_EXCLUDE_LIST.strip().lower(), default=""): cv.string, vol.Optional(CONF_DEFAULT_LABEL.strip(), default="Geen"): cv.string, vol.Optional(CONF_ID.strip().lower(), default=""): cv.string, - vol.Optional(CONF_EXCLUDE_LIST.strip().lower(), default=""): cv.string, } ) @@ -51,54 +52,66 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - provider = config.get(CONF_PROVIDER) + provider = config.get(CONF_COLLECTOR) postal_code = config.get(CONF_POSTAL_CODE) street_number = config.get(CONF_STREET_NUMBER) suffix = config.get(CONF_SUFFIX) - include_date_today = config.get(CONF_INCLUDE_DATE_TODAY) - default_label = config.get(CONF_DEFAULT_LABEL) + exclude_pickup_today = config.get(CONF_EXCLUDE_PICKUP_TODAY) exclude_list = config.get(CONF_EXCLUDE_LIST) + default_label = config.get(CONF_DEFAULT_LABEL) _LOGGER.debug("Afvalwijzer provider = %s", provider) _LOGGER.debug("Afvalwijzer zipcode = %s", postal_code) _LOGGER.debug("Afvalwijzer street_number = %s", street_number) try: - afvalwijzer = await hass.async_add_executor_job( - partial( - AfvalWijzer, - provider, - postal_code, - street_number, - suffix, - include_date_today, - default_label, - exclude_list, + if provider in SENSOR_COLLECTORS_AFVALWIJZER: + collector = await hass.async_add_executor_job( + partial( + MijnAfvalWijzerCollector, + provider, + postal_code, + street_number, + suffix, + exclude_pickup_today, + exclude_list, + default_label, + ) + ) + elif provider in SENSOR_COLLECTORS_XIMMIO.keys(): + collector = await hass.async_add_executor_job( + partial( + XimmioCollector, + provider, + postal_code, + street_number, + suffix, + exclude_pickup_today, + exclude_list, + default_label, + ) ) - ) except ValueError as err: _LOGGER.error("Check afvalwijzer platform settings %s", err.args) - raise - fetch_afvalwijzer_data = AfvalwijzerData(config) + if collector == "": + raise ValueError("Invalid provider: %s, please verify", provider) + + fetch_data = AfvalwijzerData(config) - waste_types_provider = afvalwijzer.waste_types_provider + waste_types_provider = collector.waste_types_provider _LOGGER.debug("Generating waste_types_provider list = %s", waste_types_provider) - waste_types_custom = afvalwijzer.waste_types_custom + waste_types_custom = collector.waste_types_custom _LOGGER.debug("Generating waste_types_custom list = %s", waste_types_custom) - entities = [] + entities = list() for waste_type in waste_types_provider: _LOGGER.debug("Adding sensor provider: %s", waste_type) - entities.append( - AfvalwijzerProviderSensor(hass, waste_type, fetch_afvalwijzer_data, config) - ) + entities.append(ProviderSensor(hass, waste_type, fetch_data, config)) for waste_type in waste_types_custom: _LOGGER.debug("Adding sensor custom: %s", waste_type) - entities.append( - AfvalwijzerCustomSensor(hass, waste_type, fetch_afvalwijzer_data, config) - ) + entities.append(CustomSensor(hass, waste_type, fetch_data, config)) _LOGGER.debug("Entities appended = %s", entities) async_add_entities(entities) @@ -110,48 +123,55 @@ def __init__(self, config): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - provider = self.config.get(CONF_PROVIDER) + provider = self.config.get(CONF_COLLECTOR) postal_code = self.config.get(CONF_POSTAL_CODE) street_number = self.config.get(CONF_STREET_NUMBER) suffix = self.config.get(CONF_SUFFIX) - include_date_today = self.config.get(CONF_INCLUDE_DATE_TODAY) + exclude_pickup_today = self.config.get(CONF_EXCLUDE_PICKUP_TODAY) default_label = self.config.get(CONF_DEFAULT_LABEL) exclude_list = self.config.get(CONF_EXCLUDE_LIST) try: - afvalwijzer = AfvalWijzer( - provider, - postal_code, - street_number, - suffix, - include_date_today, - default_label, - exclude_list, - ) + if provider in SENSOR_COLLECTORS_AFVALWIJZER: + collector = MijnAfvalWijzerCollector( + provider, + postal_code, + street_number, + suffix, + exclude_pickup_today, + exclude_list, + default_label, + ) + elif provider in SENSOR_COLLECTORS_XIMMIO.keys(): + collector = XimmioCollector( + provider, + postal_code, + street_number, + suffix, + exclude_pickup_today, + exclude_list, + default_label, + ) except ValueError as err: _LOGGER.error("Check afvalwijzer platform settings %s", err.args) - raise # waste data provider update - with today try: - self.waste_data_with_today = afvalwijzer.waste_data_with_today + self.waste_data_with_today = collector.waste_data_with_today except ValueError as err: _LOGGER.error("Check waste_data_provider %s", err.args) self.waste_data_with_today = default_label - raise # waste data provider update - without today try: - self.waste_data_without_today = afvalwijzer.waste_data_without_today + self.waste_data_without_today = collector.waste_data_without_today except ValueError as err: _LOGGER.error("Check waste_data_provider %s", err.args) self.waste_data_without_today = default_label - raise # waste data custom update try: - self.waste_data_custom = afvalwijzer.waste_data_custom + self.waste_data_custom = collector.waste_data_custom except ValueError as err: _LOGGER.error("Check waste_data_custom %s", err.args) self.waste_data_custom = default_label - raise diff --git a/custom_components/afvalwijzer/sensor_custom.py b/custom_components/afvalwijzer/sensor_custom.py index f49251d..3a866fa 100755 --- a/custom_components/afvalwijzer/sensor_custom.py +++ b/custom_components/afvalwijzer/sensor_custom.py @@ -11,7 +11,6 @@ ATTR_YEAR_MONTH_DAY_DATE, CONF_DEFAULT_LABEL, CONF_ID, - CONF_INCLUDE_DATE_TODAY, CONF_POSTAL_CODE, CONF_STREET_NUMBER, CONF_SUFFIX, @@ -22,16 +21,14 @@ ) -class AfvalwijzerCustomSensor(Entity): - def __init__(self, hass, waste_type, fetch_afvalwijzer_data, config): +class CustomSensor(Entity): + def __init__(self, hass, waste_type, fetch_data, config): self.hass = hass self.waste_type = waste_type - self.fetch_afvalwijzer_data = fetch_afvalwijzer_data + self.fetch_data = fetch_data self.config = config - self._id_name = self.config.get(CONF_ID) self._default_label = self.config.get(CONF_DEFAULT_LABEL) - self._last_update = None self._name = ( SENSOR_PREFIX @@ -65,7 +62,7 @@ def state(self): @property def extra_state_attributes(self): - if self._year_month_day_date != None: + if self._year_month_day_date is not None: return { ATTR_LAST_UPDATE: self._last_update, ATTR_YEAR_MONTH_DAY_DATE: self._year_month_day_date, @@ -77,9 +74,9 @@ def extra_state_attributes(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): - await self.hass.async_add_executor_job(self.fetch_afvalwijzer_data.update) + await self.hass.async_add_executor_job(self.fetch_data.update) - waste_data_custom = self.fetch_afvalwijzer_data.waste_data_custom + waste_data_custom = self.fetch_data.waste_data_custom try: # Add attribute, set the last updated status of the sensor diff --git a/custom_components/afvalwijzer/sensor_provider.py b/custom_components/afvalwijzer/sensor_provider.py index ecd1098..da29431 100755 --- a/custom_components/afvalwijzer/sensor_provider.py +++ b/custom_components/afvalwijzer/sensor_provider.py @@ -14,8 +14,8 @@ ATTR_LAST_UPDATE, ATTR_YEAR_MONTH_DAY_DATE, CONF_DEFAULT_LABEL, + CONF_EXCLUDE_PICKUP_TODAY, CONF_ID, - CONF_INCLUDE_DATE_TODAY, CONF_POSTAL_CODE, CONF_STREET_NUMBER, CONF_SUFFIX, @@ -26,17 +26,15 @@ ) -class AfvalwijzerProviderSensor(Entity): - def __init__(self, hass, waste_type, fetch_afvalwijzer_data, config): +class ProviderSensor(Entity): + def __init__(self, hass, waste_type, fetch_data, config): self.hass = hass self.waste_type = waste_type - self.fetch_afvalwijzer_data = fetch_afvalwijzer_data + self.fetch_data = fetch_data self.config = config - self._id_name = self.config.get(CONF_ID) self._default_label = self.config.get(CONF_DEFAULT_LABEL) - self._include_date_today = self.config.get(CONF_INCLUDE_DATE_TODAY) - + self._exclude_pickup_today = self.config.get(CONF_EXCLUDE_PICKUP_TODAY) self._name = ( SENSOR_PREFIX + (self._id_name + " " if len(self._id_name) > 0 else "") @@ -85,12 +83,12 @@ def extra_state_attributes(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): - await self.hass.async_add_executor_job(self.fetch_afvalwijzer_data.update) + await self.hass.async_add_executor_job(self.fetch_data.update) - if self._include_date_today.casefold() in ("true", "yes"): - waste_data_provider = self.fetch_afvalwijzer_data.waste_data_with_today + if self._exclude_pickup_today.casefold() in ("false", "no"): + waste_data_provider = self.fetch_data.waste_data_with_today else: - waste_data_provider = self.fetch_afvalwijzer_data.waste_data_without_today + waste_data_provider = self.fetch_data.waste_data_without_today try: if waste_data_provider: diff --git a/custom_components/afvalwijzer/test/dummy_data.json b/custom_components/afvalwijzer/test/dummy_data.json new file mode 100644 index 0000000..a6f35b6 --- /dev/null +++ b/custom_components/afvalwijzer/test/dummy_data.json @@ -0,0 +1,397 @@ +[ + { + "nameType": "gft", + "type": "gft", + "date": "2021-01-02" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-01-05" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-01-08" + }, + { + "nameType": "kerstbomen", + "type": "kerstbomen", + "date": "2021-01-09" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-01-15" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-01-19" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-01-20" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-01-29" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-02-02" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-02-05" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-02-12" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-02-16" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-02-17" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-02-26" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-03-02" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-03-05" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-03-12" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-03-16" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-03-17" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-03-26" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-03-30" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-04-02" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-04-09" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-04-13" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-04-21" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-04-23" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-04-30" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-04-30" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-05-07" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-05-11" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-05-19" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-05-21" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-05-25" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-05-28" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-06-04" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-06-08" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-06-16" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-06-18" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-06-22" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-06-25" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-07-02" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-07-06" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-07-16" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-07-20" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-07-21" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-07-23" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-07-30" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-08-03" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-08-13" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-08-17" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-08-18" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-08-20" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-08-27" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-08-31" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-09-10" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-09-14" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-09-15" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-09-17" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-09-24" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-09-28" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-10-08" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-10-12" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-10-15" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-10-20" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-10-22" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-10-26" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-11-05" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-11-09" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-11-12" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-11-17" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-11-19" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-11-19" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-12-03" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-12-07" + }, + { + "nameType": "restafval", + "type": "restafval", + "date": "2021-12-10" + }, + { + "nameType": "papier", + "type": "papier", + "date": "2021-12-15" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-12-17" + }, + { + "nameType": "pmd", + "type": "pmd", + "date": "2021-12-21" + }, + { + "nameType": "gft", + "type": "gft", + "date": "2021-12-31" + } +] \ No newline at end of file diff --git a/custom_components/afvalwijzer/test/test_module.py b/custom_components/afvalwijzer/test/test_module.py new file mode 100644 index 0000000..bd2845b --- /dev/null +++ b/custom_components/afvalwijzer/test/test_module.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +""" +Sensor component for AfvalDienst +Author: Bram van Dartel - xirixiz + +import afvalwijzer +from afvalwijzer.collector.mijnafvalwijzer import AfvalWijzer +AfvalWijzer().get_data('','','') + +python3 -m afvalwijzer.test.test_module + +""" + +from ..collector.mijnafvalwijzer import MijnAfvalWijzerCollector +from ..collector.ximmio import XimmioCollector +from ..const.const import SENSOR_COLLECTORS_AFVALWIJZER, SENSOR_COLLECTORS_XIMMIO + +# provider = "afvalstoffendienstkalender" +# api_token = "5ef443e778f41c4f75c69459eea6e6ae0c2d92de729aa0fc61653815fbd6a8ca" + +# Afvalstoffendienstkalender +# postal_code = "5391KE" +# street_number = "1" + +# Common +suffix = "" +exclude_pickup_today = "True" +default_label = "Geen" +exclude_list = "" + +# Afvalwijzer +provider = "mijnafvalwijzer" +postal_code = "5146EG" +street_number = "6" + +# Ximmio +# provider = "meerlanden" +# postal_code = "2201XZ" +# street_number = "38" + +if provider in SENSOR_COLLECTORS_AFVALWIJZER: + collector = MijnAfvalWijzerCollector( + provider, + postal_code, + street_number, + suffix, + exclude_pickup_today, + exclude_list, + default_label, + ) +elif provider in SENSOR_COLLECTORS_XIMMIO.keys(): + collector = XimmioCollector( + provider, + postal_code, + street_number, + suffix, + exclude_pickup_today, + exclude_list, + default_label, + ) + + +# data = XimmioCollector().get_waste_data_provider("meerlanden", postal_code2, street_number2, suffix, default_label, exclude_list) +# data2 = MijnAfvalWijzerCollector().get_waste_data_provider("mijnafvalwijzer", postal_code, street_number, suffix, default_label, exclude_list) + + +######################################################################################################### +print("\n") + +print(collector.waste_data_with_today) +print(collector.waste_data_without_today) +print(collector.waste_data_custom) +print(collector.waste_types_provider) +print(collector.waste_types_custom) + +print("\n") + +# for key, value in afval1.items(): +# print(key, value) + +# print("\n") + +# for key, value in afval2.items(): +# print(key, value) diff --git a/custom_components/afvalwijzer/test_sensor.py b/custom_components/afvalwijzer/test_sensor.py deleted file mode 100755 index cfcd6ea..0000000 --- a/custom_components/afvalwijzer/test_sensor.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -""" -Sensor component for AfvalDienst -Author: Bram van Dartel - xirixiz - -import afvalwijzer -from afvalwijzer.provider.afvalwijzer import AfvalWijzer -AfvalWijzer().get_data('','','') - -python3 -m afvalwijzer.test_sensor - -""" - -from .provider.afvalwijzer import AfvalWijzer - -provider = "mijnafvalwijzer" - -# provider = "afvalstoffendienstkalender" -# api_token = "5ef443e778f41c4f75c69459eea6e6ae0c2d92de729aa0fc61653815fbd6a8ca" - -# Afvalstoffendienstkalender -# postal_code = "5391KE" -# street_number = "1" - -# Afvalwijzer -# postal_code = "5146EG" -# street_number = "1" - -# postal_code = "4707PB" -# street_number = "110" - -postal_code = "5142HG" -street_number = "4" -# Rova -# provider = "rova" -# postal_code = "3824XB" -# street_number = "2" - - -# postal_code = "4714CB" -# street_number = "57" - -# postal_code = "5473VD" -# street_number = "7" - -# postal_code = "6691XX" -# street_number = "22" - -suffix = "" -include_date_today = "False" -default_label = "Geen" -exclude_list = "gft" - - -afvalwijzer = AfvalWijzer( - provider, - postal_code, - street_number, - suffix, - include_date_today, - default_label, - exclude_list, -) - -######################################################################################################### -print("\n") - -print(afvalwijzer.waste_data_with_today) -print(afvalwijzer.waste_data_without_today) -print(afvalwijzer.waste_data_custom) -print(afvalwijzer.waste_types_provider) -print(afvalwijzer.waste_types_custom) - -print("\n") - -# for key, value in afval1.items(): -# print(key, value) - -# print("\n") - -# for key, value in afval2.items(): -# print(key, value)