From 8d4f2543040db63ffc85bd2d17ef76913d653473 Mon Sep 17 00:00:00 2001 From: Bram van Dartel Date: Thu, 5 Dec 2024 07:59:38 +0100 Subject: [PATCH] add config flow --- README.md | 167 +++++++++++------- custom_components/afvalwijzer/__init__.py | 31 ++++ .../afvalwijzer/collector/main_collector.py | 27 +-- custom_components/afvalwijzer/config_flow.py | 102 +++++++++++ custom_components/afvalwijzer/const/const.py | 6 +- custom_components/afvalwijzer/manifest.json | 11 +- custom_components/afvalwijzer/sensor.py | 148 ++++++++-------- .../afvalwijzer/sensor_custom.py | 62 ++++--- .../afvalwijzer/sensor_provider.py | 85 +++++---- custom_components/afvalwijzer/strings.json | 4 + .../afvalwijzer/translations/en.json | 41 +++++ .../afvalwijzer/translations/nl.json | 41 +++++ 12 files changed, 505 insertions(+), 220 deletions(-) mode change 100755 => 100644 custom_components/afvalwijzer/__init__.py create mode 100644 custom_components/afvalwijzer/config_flow.py mode change 100755 => 100644 custom_components/afvalwijzer/const/const.py mode change 100755 => 100644 custom_components/afvalwijzer/sensor.py mode change 100755 => 100644 custom_components/afvalwijzer/sensor_custom.py mode change 100755 => 100644 custom_components/afvalwijzer/sensor_provider.py create mode 100644 custom_components/afvalwijzer/strings.json create mode 100644 custom_components/afvalwijzer/translations/en.json create mode 100644 custom_components/afvalwijzer/translations/nl.json diff --git a/README.md b/README.md index 8adf2e1..482acb5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Afvalwijzer + [![custom_updater][customupdaterbadge]][customupdater] [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/hacs/integration) @@ -8,11 +9,10 @@

[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/xirixiz) - _Component to integrate with the following providers._ | Provider | -| ---------------------------------| +| -------------------------------- | | acv | | afvalstoffendienstkalender (all) | | alkmaar | @@ -64,7 +64,7 @@ _Component to integrate with the following providers._ | ximmio | | zrd | -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. +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. **This component will set up the following platform(s).** @@ -75,69 +75,100 @@ This custom component dynamically creates sensor.afvalwijzer_* items. For me per ![example][exampleimg1] The second row sorts the waste items by date using the following lovelace code + ```yaml - - type: custom:auto-entities - card: - type: glance - filter: - exclude: - - entity_id: sensor.afvalwijzer_*next* - - entity_id: sensor.afvalwijzer_day_after_tomorrow* - - entity_id: sensor.afvalwijzer_today* - - entity_id: sensor.afvalwijzer_tomorrow* - - entity_id: sensor.afvalwijzer_kerstbomen* - - entity_id: sensor.afvalwijzer_*orgen - - entity_id: sensor.afvalwijzer_van* - include: - - entity_id: sensor.afvalwijzer_* - options: - format: date - sort: - method: state - - entities: - - style: - background: '#62717b' - height: 1px - margin-left: auto - margin-right: auto - type: divider - type: entities - - type: markdown - content: >- -
De volgende leging is {{ states('sensor.afvalwijzer_next_type') - }}. Dat is over {{ states('sensor.afvalwijzer_next_in_days') }} {% if - is_state('sensor.afvalwijzer_next_in_days', '1') %}dag{% else %}dagen{% - endif %}.
+- type: custom:auto-entities + card: + type: glance + filter: + exclude: + - entity_id: sensor.afvalwijzer_*next* + - entity_id: sensor.afvalwijzer_day_after_tomorrow* + - entity_id: sensor.afvalwijzer_today* + - entity_id: sensor.afvalwijzer_tomorrow* + - entity_id: sensor.afvalwijzer_kerstbomen* + - entity_id: sensor.afvalwijzer_*orgen + - entity_id: sensor.afvalwijzer_van* + include: + - entity_id: sensor.afvalwijzer_* + options: + format: date + sort: + method: state +- entities: + - style: + background: '#62717b' + height: 1px + margin-left: auto + margin-right: auto + type: divider + type: entities +- type: markdown + content: >- +
De volgende leging is {{ states('sensor.afvalwijzer_next_type') + }}. Dat is over {{ states('sensor.afvalwijzer_next_in_days') }} {% if + is_state('sensor.afvalwijzer_next_in_days', '1') %}dag{% else %}dagen{% + endif %}.
``` More information on the reminders (ios in this case): + - https://github.com/xirixiz/my-hass-config/blob/master/packages/waste.yaml - https://github.com/xirixiz/my-hass-config/blob/05d8755a737676b60faac98dc0cce91d06277939/configuration.yaml#L73 ## Installation -1. Using your tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). -2. If you do not have a `custom_components` directory (folder) there, you need to create it. -3. In the `custom_components` directory (folder) create a new folder called `afvalwijzer`. -4. Download _all_ the files from the `custom_components/afvalwijzer/` directory (folder) in this repository. -5. Place the files you downloaded in the new directory (folder) you created. -6. Restart Home Assistant before further configuration. -7. Look at the `Example Configuration` section for further configuration. -8. Restart Home Assistant again when configuration is done to activate the configuration. - -Using your HA configuration directory (folder) as a starting point you should now also have this: - -```text -custom_components/afvalwijzer/__init__.py -custom_components/afvalwijzer/manifest.json -custom_components/afvalwijzer/sensor.py +### Manual Installation + +1. Navigate to your Home Assistant configuration directory (where your `configuration.yaml` is located). +2. Create a folder named `custom_components` if it doesn't exist. +3. Inside the `custom_components` folder, create another folder named `afvalwijzer`. +4. Clone this repository or download the source code and copy all files from the `custom_components/afvalwijzer/` directory to the newly created `afvalwijzer` folder. +5. Restart Home Assistant to load the custom component. + +After following these steps, your directory structure should look like this: + +```markdown +custom_components/ +afvalwijzer/ +**init**.py +manifest.json +sensor.py +config_flow.py +... ``` +### Installation via HACS + +1. Ensure HACS is installed in your Home Assistant setup. If not, follow the [HACS installation guide](https://hacs.xyz/docs/setup/download). +2. Open the HACS panel in Home Assistant. +3. Click on the `Frontend` or `Integrations` tab. +4. Click the `+` button and search for `Afvalwijzer`. +5. Click `Install` to add the component to your Home Assistant setup. +6. Restart Home Assistant after the installation completes. + +--- + +## Configuration + +### Add Integration + +1. Go to the **Settings** → **Devices & Services** page in Home Assistant. +2. Click **Add Integration** and search for `Afvalwijzer`. +3. Follow the on-screen instructions to complete the setup. + - Provide your postal code, street number, and any other required details. + +After completing the config flow, the integration will dynamically create sensors for waste collection dates based on your chosen provider. + +--- + ##### CUSTOM COMPONENT USAGE + https://github.com/home-assistant/example-custom-config/tree/master/custom_components/example_sensor ##### LOGLEVEL -In order to extend the log level, modify the following (configuration.yaml probably) + +To enable debug logging for troubleshooting, add the following lines to your `configuration.yaml`: ```yaml logger: @@ -150,22 +181,23 @@ logger: Here's an example of my own Home Asisstant config: https://github.com/xirixiz/home-assistant - ###### SENSOR - CONFIGURATION.YAML + ```yaml - sensor: - - platform: afvalwijzer - provider: mijnafvalwijzer # (required, default = mijnafvalwijzer) either choose mijnafvalwijzer, afvalstoffendienstkalender or rova - postal_code: 1234AB # (required, default = '') - street_number: 5 # (required, default = '') - suffix: '' # (optional, default = '') - 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 waste types (case ignored). F.e. "papier, gft" +sensor: + - platform: afvalwijzer + provider: mijnafvalwijzer # (required, default = mijnafvalwijzer) either choose mijnafvalwijzer, afvalstoffendienstkalender or rova + postal_code: 1234AB # (required, default = '') + street_number: 5 # (required, default = '') + suffix: '' # (optional, default = '') + 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 waste types (case ignored). F.e. "papier, gft" ``` ###### INPUT BOOLEAN (FOR AUTOMATION) + ```yaml input_boolean: waste_moved: @@ -178,6 +210,7 @@ input_boolean: ``` ###### AUTOMATION + ```yaml automation: - alias: Reset waste notification @@ -206,7 +239,7 @@ automation: - alias: Waste has not been moved trigger: platform: time_pattern - hours: "/1" + hours: '/1' condition: condition: and conditions: @@ -224,15 +257,15 @@ automation: action: - service: notify.family data: - title: "Afval" + title: 'Afval' message: 'Het is vandaag - {{ now().strftime("%d-%m-%Y") }}. Afvaltype(n): {{ states("sensor.afvalwijzer_tomorrow") }} wordt opgehaald op: {{ (now() + timedelta(days=1)).strftime("%d-%m-%Y") }}!' data: actions: - - action: "MARK_WASTE_MOVED" # The key you are sending for the event - title: "Afval buiten gezet" # The button title + - action: 'MARK_WASTE_MOVED' # The key you are sending for the event + title: 'Afval buiten gezet' # The button title ``` -*** +--- [exampleimg1]: afvalwijzer-lovelace.png [exampleimg2]: afvalwijzer_lovelace.png diff --git a/custom_components/afvalwijzer/__init__.py b/custom_components/afvalwijzer/__init__.py old mode 100755 new mode 100644 index e69de29..496b7de --- a/custom_components/afvalwijzer/__init__.py +++ b/custom_components/afvalwijzer/__init__.py @@ -0,0 +1,31 @@ +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from .const.const import DOMAIN + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Afvalwijzer integration.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Afvalwijzer from a config entry.""" + # Store config entry data + hass.data[DOMAIN][entry.entry_id] = entry.data + + # Forward the setup to the sensor platform + await hass.config_entries.async_forward_entry_setups(entry, ["sensor"]) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + # Remove stored data + if entry.entry_id in hass.data[DOMAIN]: + hass.data[DOMAIN].pop(entry.entry_id) + + # Unload the sensor platform + await hass.config_entries.async_forward_entry_unload(entry, "sensor") + return True diff --git a/custom_components/afvalwijzer/collector/main_collector.py b/custom_components/afvalwijzer/collector/main_collector.py index c9a4b08..8574ad7 100644 --- a/custom_components/afvalwijzer/collector/main_collector.py +++ b/custom_components/afvalwijzer/collector/main_collector.py @@ -29,15 +29,21 @@ def __init__( exclude_list, default_label, ): - self.provider = provider.strip().lower() - self.postal_code = postal_code.strip().upper() - self.street_number = street_number.strip() - self.suffix = suffix.strip().lower() - self.exclude_pickup_today = exclude_pickup_today.strip() - self.date_isoformat = date_isoformat.strip() - self.exclude_list = exclude_list.strip().lower() - self.default_label = default_label.strip() + # Ensure provider and address fields are strings + self.provider = str(provider).strip().lower() + self.postal_code = str(postal_code).strip().upper() + self.street_number = str(street_number).strip() + self.suffix = str(suffix).strip().lower() + # Handle boolean and string parameters correctly + self.exclude_pickup_today = str(exclude_pickup_today).lower() if isinstance( + exclude_pickup_today, bool) else str(exclude_pickup_today).strip().lower() + self.date_isoformat = str(date_isoformat).lower() if isinstance( + date_isoformat, bool) else str(date_isoformat).strip().lower() + self.exclude_list = str(exclude_list).strip().lower() + self.default_label = str(default_label).strip() + + # Validate and process the provider try: if provider in SENSOR_COLLECTORS_AFVALWIJZER: waste_data_raw = mijnafvalwijzer.get_waste_data_raw( @@ -97,10 +103,11 @@ def __init__( ) else: _LOGGER.error(f"Unknown provider: {provider}") - return False + raise ValueError(f"Unknown provider: {provider}") except ValueError as err: - _LOGGER.error(f"Check afvalwijzer platform settings {err.args}") + _LOGGER.error(f"Check afvalwijzer platform settings: {err}") + raise ########################################################################## # COMMON CODE diff --git a/custom_components/afvalwijzer/config_flow.py b/custom_components/afvalwijzer/config_flow.py new file mode 100644 index 0000000..414c8fd --- /dev/null +++ b/custom_components/afvalwijzer/config_flow.py @@ -0,0 +1,102 @@ +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .const.const import ( + DOMAIN, + CONF_COLLECTOR, + CONF_POSTAL_CODE, + CONF_STREET_NUMBER, + CONF_SUFFIX, + CONF_EXCLUDE_PICKUP_TODAY, + CONF_DATE_ISOFORMAT, + CONF_DEFAULT_LABEL, + CONF_EXCLUDE_LIST, +) + +DATA_SCHEMA = vol.Schema({ + vol.Required(CONF_COLLECTOR): cv.string, + vol.Required(CONF_POSTAL_CODE): cv.string, + vol.Required(CONF_STREET_NUMBER): cv.string, + vol.Optional(CONF_SUFFIX, 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, + vol.Optional(CONF_EXCLUDE_LIST, default=""): cv.string, +}) + + +class AfvalwijzerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Afvalwijzer.""" + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + # Perform validation + if not self._validate_postal_code(user_input.get(CONF_POSTAL_CODE)): + errors["postal_code"] = "config.error.invalid_postal_code" + elif not self._validate_street_number(user_input.get(CONF_STREET_NUMBER)): + errors["street_number"] = "config.error.invalid_street_number" + else: + # Validation passed, create the entry + return self.async_create_entry(title="Afvalwijzer", data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + description_placeholders={ + "provider": "e.g., mijnafvalwijzer", + "postal_code": "e.g., 1234AB", + }, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + return AfvalwijzerOptionsFlow(config_entry) + + def _validate_postal_code(self, postal_code): + """Validate the postal code format.""" + return ( + isinstance(postal_code, str) + and len(postal_code) == 6 + and postal_code[:4].isdigit() + and postal_code[4:].isalpha() + ) + + def _validate_street_number(self, street_number): + """Validate the street number.""" + return street_number.isdigit() + + +class AfvalwijzerOptionsFlow(config_entries.OptionsFlow): + """Handle options for Afvalwijzer.""" + + def __init__(self, config_entry): + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options configuration.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options_schema = vol.Schema({ + 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, + vol.Optional(CONF_EXCLUDE_LIST, default=""): cv.string, + }) + + return self.async_show_form( + step_id="init", + data_schema=options_schema, + description_placeholders={ + "exclude_pickup_today": "Exclude today's pickup", + "date_isoformat": "Use ISO date format", + }, + ) diff --git a/custom_components/afvalwijzer/const/const.py b/custom_components/afvalwijzer/const/const.py old mode 100755 new mode 100644 index 9ec161a..362b91d --- a/custom_components/afvalwijzer/const/const.py +++ b/custom_components/afvalwijzer/const/const.py @@ -5,7 +5,7 @@ API = "api" NAME = "afvalwijzer" -VERSION = "2024.06.02" +VERSION = "2024.12.01" ISSUE_URL = "https://github.com/xirixiz/homeassistant-afvalwijzer/issues" @@ -126,9 +126,7 @@ ATTR_IS_COLLECTION_DATE_DAY_AFTER_TOMORROW = "is_collection_date_day_after_tomorrow" ATTR_DAYS_UNTIL_COLLECTION_DATE = "days_until_collection_date" -MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) -PARALLEL_UPDATES = 1 -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(hours=4) DOMAIN = "afvalwijzer" DOMAIN_DATA = "afvalwijzer_data" diff --git a/custom_components/afvalwijzer/manifest.json b/custom_components/afvalwijzer/manifest.json index 343d0bb..b8ffdf6 100644 --- a/custom_components/afvalwijzer/manifest.json +++ b/custom_components/afvalwijzer/manifest.json @@ -1,14 +1,13 @@ { "domain": "afvalwijzer", "name": "Afvalwijzer", - "codeowners": [ - "@xirixiz" - ], - "config_flow": false, + "codeowners": ["@xirixiz"], + "config_flow": true, "dependencies": [], "documentation": "https://github.com/xirixiz/homeassistant-afvalwijzer/blob/main/README.md", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/xirixiz/homeassistant-afvalwijzer/issues", "requirements": [], - "version": "2024.06.02" -} \ No newline at end of file + "version": "2024.12.01", + "translations": ["strings.json"] +} diff --git a/custom_components/afvalwijzer/sensor.py b/custom_components/afvalwijzer/sensor.py old mode 100755 new mode 100644 index 6842c07..658605f --- a/custom_components/afvalwijzer/sensor.py +++ b/custom_components/afvalwijzer/sensor.py @@ -1,15 +1,13 @@ -#!/usr/bin/env python3 """ Sensor component Afvalwijzer Author: Bram van Dartel - xirixiz """ -from functools import partial - +from homeassistant.helpers.event import async_track_time_interval +from datetime import timedelta from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle import voluptuous as vol +import homeassistant.helpers.config_validation as cv from .collector.main_collector import MainCollector from .const.const import ( @@ -23,91 +21,102 @@ CONF_POSTAL_CODE, CONF_STREET_NUMBER, CONF_SUFFIX, - MIN_TIME_BETWEEN_UPDATES, - PARALLEL_UPDATES, SCAN_INTERVAL, - STARTUP_MESSAGE, ) from .sensor_custom import CustomSensor from .sensor_provider import ProviderSensor +# Define the platform schema for validation PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional( - CONF_COLLECTOR, default="mijnafvalwijzer" - ): cv.string, - vol.Required(CONF_POSTAL_CODE, default="1234AB"): cv.string, - vol.Required(CONF_STREET_NUMBER, default="5"): cv.string, + vol.Optional(CONF_COLLECTOR, default="mijnafvalwijzer"): cv.string, + vol.Required(CONF_POSTAL_CODE): cv.string, + vol.Required(CONF_STREET_NUMBER): cv.string, vol.Optional(CONF_SUFFIX, default=""): cv.string, - vol.Optional(CONF_EXCLUDE_PICKUP_TODAY, default="true"): cv.string, - vol.Optional(CONF_DATE_ISOFORMAT, default="false"): 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, vol.Optional(CONF_DEFAULT_LABEL, default="geen"): cv.string, - vol.Optional(CONF_ID.strip().lower(), default=""): cv.string, + vol.Optional(CONF_ID, default=""): cv.string, } ) -_LOGGER.info(STARTUP_MESSAGE) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up sensors using the platform schema.""" + if not discovery_info: + _LOGGER.error( + "No discovery information provided; sensors cannot be created.") + return + + await _setup_sensors(hass, discovery_info, async_add_entities) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up sensors from a config entry.""" + await _setup_sensors(hass, entry.data, async_add_entities) + + +async def _setup_sensors(hass, config, async_add_entities): + """Common setup logic for platform and config entry.""" provider = config.get(CONF_COLLECTOR) postal_code = config.get(CONF_POSTAL_CODE) street_number = config.get(CONF_STREET_NUMBER) - suffix = config.get(CONF_SUFFIX) - exclude_pickup_today = config.get(CONF_EXCLUDE_PICKUP_TODAY) - date_isoformat = config.get(CONF_DATE_ISOFORMAT) - exclude_list = config.get(CONF_EXCLUDE_LIST) - default_label = config.get(CONF_DEFAULT_LABEL) + suffix = config.get(CONF_SUFFIX, "") + 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, "") + default_label = config.get(CONF_DEFAULT_LABEL, "geen") - _LOGGER.debug(f"Afvalwijzer provider = {provider}") - _LOGGER.debug(f"Afvalwijzer zipcode = {postal_code}") - _LOGGER.debug(f"Afvalwijzer street_number = {street_number}") + _LOGGER.debug(f"Setting up Afvalwijzer sensors for provider: {provider}.") - try: - collector = await hass.async_add_executor_job( - partial( - MainCollector, - provider, - postal_code, - street_number, - suffix, - exclude_pickup_today, - date_isoformat, - exclude_list, - default_label, - ) - ) - except ValueError as err: - _LOGGER.error(f"Check afvalwijzer platform settings {err.args}") + # Initialize data handler + data = AfvalwijzerData(hass, config) + + # Perform an initial update at startup + await hass.async_add_executor_job(data.update) + + # Schedule periodic updates every 4 hours + update_interval = timedelta(hours=4) + async_track_time_interval( + hass, lambda _: hass.async_add_executor_job(data.update), update_interval) - fetch_data = AfvalwijzerData(hass, config) + # Fetch waste types + try: + waste_types_provider = data.waste_data_with_today.keys() + waste_types_custom = data.waste_data_custom.keys() + except Exception as err: + _LOGGER.error(f"Failed to fetch waste types: {err}") + return - waste_types_provider = collector.waste_types_provider - _LOGGER.debug(f"Generating waste_types_provider list = {waste_types_provider}") - waste_types_custom = collector.waste_types_custom - _LOGGER.debug(f"Generating waste_types_custom list = {waste_types_custom}") + # Create entities + entities = [ + ProviderSensor(hass, waste_type, data, config) for waste_type in waste_types_provider + ] + [ + CustomSensor(hass, waste_type, data, config) for waste_type in waste_types_custom + ] - entities = [] + if not entities: + _LOGGER.error( + "No entities created; check configuration or collector output.") + return - for waste_type in waste_types_provider: - _LOGGER.debug(f"Adding sensor provider: {waste_type}") - entities.append(ProviderSensor(hass, waste_type, fetch_data, config)) - for waste_type in waste_types_custom: - _LOGGER.debug(f"Adding sensor custom: {waste_type}") - entities.append(CustomSensor(hass, waste_type, fetch_data, config)) + _LOGGER.info(f"Adding {len(entities)} sensors for Afvalwijzer.") + async_add_entities(entities, True) - _LOGGER.debug(f"Entities appended = {entities}") - async_add_entities(entities) +class AfvalwijzerData: + """Class to handle fetching and storing Afvalwijzer data.""" -class AfvalwijzerData(object): def __init__(self, hass, config): - self._hass = hass + self.hass = hass self.config = config + self.waste_data_with_today = None + self.waste_data_without_today = None + self.waste_data_custom = None - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): + """Fetch the latest waste data.""" provider = self.config.get(CONF_COLLECTOR) postal_code = self.config.get(CONF_POSTAL_CODE) street_number = self.config.get(CONF_STREET_NUMBER) @@ -129,25 +138,14 @@ def update(self): default_label, ) except ValueError as err: - _LOGGER.error(f"Check afvalwijzer platform settings {err.args}") + _LOGGER.error(f"Collector initialization failed: {err}") + return - # waste data provider update - with today try: self.waste_data_with_today = collector.waste_data_with_today - except ValueError as err: - _LOGGER.error(f"Check waste_data_provider {err.args}") - self.waste_data_with_today = default_label - - # waste data provider update - without today - try: self.waste_data_without_today = collector.waste_data_without_today - except ValueError as err: - _LOGGER.error(f"Check waste_data_provider {err.args}") - self.waste_data_without_today = default_label - - # waste data custom update - try: self.waste_data_custom = collector.waste_data_custom + _LOGGER.debug("Waste data updated successfully.") except ValueError as err: - _LOGGER.error(f"Check waste_data_custom {err.args}") - self.waste_data_custom = default_label + _LOGGER.error(f"Failed to fetch waste data: {err}") + self.waste_data_with_today = self.waste_data_without_today = self.waste_data_custom = default_label diff --git a/custom_components/afvalwijzer/sensor_custom.py b/custom_components/afvalwijzer/sensor_custom.py old mode 100755 new mode 100644 index 41d2faa..3100ed2 --- a/custom_components/afvalwijzer/sensor_custom.py +++ b/custom_components/afvalwijzer/sensor_custom.py @@ -1,6 +1,5 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.components.sensor import SensorEntity, SensorDeviceClass -from homeassistant.util import Throttle from datetime import datetime, date import hashlib @@ -15,30 +14,32 @@ CONF_STREET_NUMBER, CONF_SUFFIX, CONF_DATE_ISOFORMAT, - MIN_TIME_BETWEEN_UPDATES, SENSOR_ICON, SENSOR_PREFIX, ) class CustomSensor(RestoreEntity, SensorEntity): + """Representation of a custom-based waste sensor.""" + def __init__(self, hass, waste_type, fetch_data, config): + """Initialize the sensor.""" self.hass = hass self.waste_type = waste_type - self.fetch_data = fetch_data + self.fetch_data = fetch_data # Should be an instance of AfvalwijzerData self.config = config self._id_name = config.get(CONF_ID) self._default_label = config.get(CONF_DEFAULT_LABEL) + self._date_isoformat = str(config.get(CONF_DATE_ISOFORMAT)).lower() self._last_update = None self._days_until_collection_date = None - self._date_isoformat = config.get(CONF_DATE_ISOFORMAT) self._name = ( SENSOR_PREFIX + (f"{self._id_name} " if self._id_name else "") ) + waste_type - self._state = config.get(CONF_DEFAULT_LABEL) + 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, '')}".encode( "utf-8" ) ).hexdigest() @@ -46,26 +47,32 @@ def __init__(self, hass, waste_type, fetch_data, config): @property def name(self): + """Return the name of the sensor.""" return self._name @property def unique_id(self): + """Return a unique ID for the sensor.""" return self._unique_id @property def icon(self): + """Return the icon of the sensor.""" return self._icon @property def state(self): + """Return the state of the sensor.""" return self._state @property def device_class(self): + """Return the device class of the sensor.""" return self._device_class @property def state_attributes(self): + """Return the attributes of the sensor.""" attrs = { ATTR_LAST_UPDATE: self._last_update, } @@ -75,43 +82,52 @@ def state_attributes(self): attrs["device_class"] = self._device_class return attrs - @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): - await self.hass.async_add_executor_job(self.fetch_data.update) - - waste_data_custom = self.fetch_data.waste_data_custom + """Fetch the latest data and update the state.""" + _LOGGER.debug(f"Updating custom sensor: {self.name}") try: - self._last_update = datetime.now().isoformat() + # Call update method from fetch_data + await self.hass.async_add_executor_job(self.fetch_data.update) + + # Get waste data for custom sensors + waste_data_custom = self.fetch_data.waste_data_custom + + if not waste_data_custom or self.waste_type not in waste_data_custom: + raise ValueError(f"No data for waste type: {self.waste_type}") - if isinstance(waste_data_custom[self.waste_type], datetime): - self._update_attributes_date(waste_data_custom[self.waste_type]) + # Update attributes and state based on waste data + collection_date = waste_data_custom[self.waste_type] + if isinstance(collection_date, datetime): + self._update_attributes_date(collection_date) else: - self._update_attributes_non_date(waste_data_custom[self.waste_type]) - except ValueError: - _LOGGER.debug("ValueError AfvalwijzerCustomSensor - unable to set value!") + self._update_attributes_non_date(collection_date) + + except Exception as err: + _LOGGER.error(f"Error updating custom sensor {self.name}: {err}") self._handle_value_error() def _update_attributes_date(self, collection_date): - if self._date_isoformat.casefold() in ("true", "yes"): - collection_date_object = collection_date.isoformat() - else: - collection_date_object = collection_date.date() - + """Update attributes for a datetime value.""" + collection_date_object = ( + collection_date.isoformat() if self._date_isoformat in ( + "true", "yes") else collection_date.date() + ) collection_date_delta = collection_date.date() delta = collection_date_delta - date.today() - self._days_until_collection_date = delta.days + self._days_until_collection_date = delta.days self._device_class = SensorDeviceClass.TIMESTAMP - self._state = collection_date_object def _update_attributes_non_date(self, value): + """Update attributes for a non-datetime value.""" self._state = str(value) self._days_until_collection_date = None self._device_class = None def _handle_value_error(self): + """Handle errors in fetching data.""" self._state = self._default_label self._days_until_collection_date = None self._device_class = None diff --git a/custom_components/afvalwijzer/sensor_provider.py b/custom_components/afvalwijzer/sensor_provider.py old mode 100755 new mode 100644 index 2711116..3cf16cf --- a/custom_components/afvalwijzer/sensor_provider.py +++ b/custom_components/afvalwijzer/sensor_provider.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -from homeassistant.util import Throttle from homeassistant.components.sensor import SensorEntity, SensorDeviceClass from homeassistant.helpers.restore_state import RestoreEntity from datetime import datetime, date, timedelta @@ -19,22 +18,24 @@ CONF_STREET_NUMBER, CONF_SUFFIX, CONF_DATE_ISOFORMAT, - MIN_TIME_BETWEEN_UPDATES, - PARALLEL_UPDATES, SENSOR_ICON, SENSOR_PREFIX, ) class ProviderSensor(RestoreEntity, SensorEntity): + """Representation of a provider-based waste sensor.""" + def __init__(self, hass, waste_type, fetch_data, config): + """Initialize the sensor.""" self.hass = hass self.waste_type = waste_type - self.fetch_data = fetch_data + self.fetch_data = fetch_data # This should be an instance of AfvalwijzerData self.config = config self._id_name = config.get(CONF_ID) self._default_label = config.get(CONF_DEFAULT_LABEL) - self._exclude_pickup_today = config.get(CONF_EXCLUDE_PICKUP_TODAY) + self._exclude_pickup_today = str( + config.get(CONF_EXCLUDE_PICKUP_TODAY)).lower() self._name = ( SENSOR_PREFIX + (f"{self._id_name} " if self._id_name else "") ) + waste_type @@ -43,11 +44,11 @@ def __init__(self, hass, waste_type, fetch_data, config): self._is_collection_date_today = False self._is_collection_date_tomorrow = False self._is_collection_date_day_after_tomorrow = False - self._date_isoformat = config.get(CONF_DATE_ISOFORMAT) - self._state = config.get(CONF_DEFAULT_LABEL) + self._date_isoformat = str(config.get(CONF_DATE_ISOFORMAT)).lower() + 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, '')}".encode( "utf-8" ) ).hexdigest() @@ -55,86 +56,100 @@ def __init__(self, hass, waste_type, fetch_data, config): @property def name(self): + """Return the name of the sensor.""" return self._name @property def unique_id(self): + """Return a unique ID for the sensor.""" return self._unique_id @property def icon(self): + """Return the icon of the sensor.""" return self._icon @property def state(self): + """Return the state of the sensor.""" return self._state @property def device_class(self): + """Return the device class of the sensor.""" return self._device_class @property def state_attributes(self): - attrs = { + """Return the attributes of the sensor.""" + return { ATTR_LAST_UPDATE: self._last_update, ATTR_DAYS_UNTIL_COLLECTION_DATE: self._days_until_collection_date, ATTR_IS_COLLECTION_DATE_TODAY: self._is_collection_date_today, ATTR_IS_COLLECTION_DATE_TOMORROW: self._is_collection_date_tomorrow, ATTR_IS_COLLECTION_DATE_DAY_AFTER_TOMORROW: self._is_collection_date_day_after_tomorrow, } - if isinstance(self._state, datetime): - attrs["device_class"] = self._device_class - return attrs - @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): - await self.hass.async_add_executor_job(self.fetch_data.update) - - waste_data_provider = ( - self.fetch_data.waste_data_with_today - if self._exclude_pickup_today.casefold() in ("false", "no") - else self.fetch_data.waste_data_without_today - ) + """Fetch the latest data and update the state.""" + _LOGGER.debug(f"Updating sensor: {self.name}") try: + # Call update method from fetch_data + await self.hass.async_add_executor_job(self.fetch_data.update) + + # Select the correct waste data based on exclude_pickup_today + waste_data_provider = ( + self.fetch_data.waste_data_with_today + if self._exclude_pickup_today in ("false", "no") + else self.fetch_data.waste_data_without_today + ) + if not waste_data_provider or self.waste_type not in waste_data_provider: - raise ValueError - self._last_update = datetime.now().isoformat() + raise ValueError(f"No data for waste type: {self.waste_type}") - if isinstance(waste_data_provider[self.waste_type], datetime): - self._update_attributes_date(waste_data_provider[self.waste_type]) + # Update attributes and state based on the waste data + collection_date = waste_data_provider[self.waste_type] + if isinstance(collection_date, datetime): + self._update_attributes_date(collection_date) else: - self._update_attributes_non_date(waste_data_provider[self.waste_type]) - except ValueError: + self._update_attributes_non_date(collection_date) + + except Exception as err: + _LOGGER.error(f"Error updating sensor {self.name}: {err}") self._handle_value_error() def _update_attributes_date(self, collection_date): - if self._date_isoformat.casefold() in ("true", "yes"): - collection_date_object = collection_date.isoformat() - else: - collection_date_object = collection_date.date() - + """Update attributes for a datetime value.""" + collection_date_object = ( + collection_date.isoformat() if self._date_isoformat in ( + "true", "yes") else collection_date.date() + ) collection_date_delta = collection_date.date() delta = collection_date_delta - date.today() - self._days_until_collection_date = delta.days + self._days_until_collection_date = delta.days self._update_collection_date_flags(collection_date_delta) self._device_class = SensorDeviceClass.TIMESTAMP - self._state = collection_date_object def _update_attributes_non_date(self, value): + """Update attributes for a non-datetime value.""" self._state = str(value) self._days_until_collection_date = None self._device_class = None def _update_collection_date_flags(self, collection_date_delta): + """Update flags for collection date.""" today = date.today() self._is_collection_date_today = collection_date_delta == today - self._is_collection_date_tomorrow = collection_date_delta == today + timedelta(days=1) - self._is_collection_date_day_after_tomorrow = collection_date_delta == today + timedelta(days=2) + self._is_collection_date_tomorrow = collection_date_delta == today + \ + timedelta(days=1) + self._is_collection_date_day_after_tomorrow = collection_date_delta == today + \ + timedelta(days=2) def _handle_value_error(self): + """Handle errors in fetching data.""" self._state = self._default_label self._is_collection_date_today = None self._is_collection_date_tomorrow = None diff --git a/custom_components/afvalwijzer/strings.json b/custom_components/afvalwijzer/strings.json new file mode 100644 index 0000000..8248a6f --- /dev/null +++ b/custom_components/afvalwijzer/strings.json @@ -0,0 +1,4 @@ +"translations": { + "en": "translations/en.json", + "nl": "translations/nl.json" +} diff --git a/custom_components/afvalwijzer/translations/en.json b/custom_components/afvalwijzer/translations/en.json new file mode 100644 index 0000000..1cfb78a --- /dev/null +++ b/custom_components/afvalwijzer/translations/en.json @@ -0,0 +1,41 @@ +{ + "config": { + "step": { + "user": { + "title": "Configure Afvalwijzer", + "description": "Set up your afvalwijzer integration.", + "data": { + "provider": "Provider (e.g., mijnafvalwijzer). See README.md.", + "postal_code": "Postal code (e.g., 1234AB)", + "street_number": "Street number", + "suffix": "Address suffix", + "exclude_pickup_today": "Exclude today's pickup", + "date_isoformat": "Use ISO date format", + "default_label": "Default label when no data is available", + "exclude_list": "Waste type exclude list" + } + } + }, + "error": { + "invalid_postal_code": "Invalid postal code format", + "invalid_street_number": "Street number must be numeric" + }, + "abort": { + "already_configured": "Afvalwijzer is already configured" + } + }, + "options": { + "step": { + "init": { + "title": "Afvalwijzer Options", + "description": "Configure options for afvalwijzer integration.", + "data": { + "exclude_pickup_today": "Exclude today's pickup", + "date_isoformat": "Use ISO date format", + "default_label": "Default label when no data is available", + "exclude_list": "Waste type exclude list" + } + } + } + } +} diff --git a/custom_components/afvalwijzer/translations/nl.json b/custom_components/afvalwijzer/translations/nl.json new file mode 100644 index 0000000..d6de13a --- /dev/null +++ b/custom_components/afvalwijzer/translations/nl.json @@ -0,0 +1,41 @@ +{ + "config": { + "step": { + "user": { + "title": "Configureer Afvalwijzer", + "description": "Stel je Afvalwijzer-integratie in.", + "data": { + "provider": "Provider (bijv. mijnafvalwijzer). Lees README.md.", + "postal_code": "Postcode (bijv. 1234AB)", + "street_number": "Huisnummer", + "suffix": "Huisnummer toevoeging", + "exclude_pickup_today": "Sluit ophalen van vandaag uit", + "date_isoformat": "Gebruik ISO-datumformaat", + "default_label": "Standaard label bij geen datum bekend", + "exclude_list": "Afvaltype uitsluiten" + } + } + }, + "error": { + "invalid_postal_code": "Ongeldig postcodeformaat", + "invalid_street_number": "Huisnummer moet numeriek zijn" + }, + "abort": { + "already_configured": "Afvalwijzer is al geconfigureerd" + } + }, + "options": { + "step": { + "init": { + "title": "Afvalwijzer Opties", + "description": "Pas extra opties aan voor de Afvalwijzer-integratie.", + "data": { + "exclude_pickup_today": "Sluit ophalen van vandaag uit", + "date_isoformat": "Gebruik ISO-datumformaat", + "default_label": "Standaard label bij geen datum bekend", + "exclude_list": "Afvaltype uitsluiten" + } + } + } + } +}