diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 0000000..337cf0c --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,38 @@ +{ + "name": "ludeeus/integration_blueprint", + "image": "mcr.microsoft.com/vscode/devcontainers/python:3.12-bullseye", + "postCreateCommand": "scripts/setup", + "forwardPorts": [ + 8123 + ], + "portsAttributes": { + "8123": { + "label": "Home Assistant", + "onAutoForward": "notify" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "github.vscode-pull-request-github", + "ryanluker.vscode-coverage-gutters", + "ms-python.vscode-pylance" + ], + "settings": { + "files.eol": "\n", + "editor.tabSize": 4, + "python.pythonPath": "/usr/bin/python3", + "python.analysis.autoSearchPaths": false, + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true + } + } + }, + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/rust:1": {} + } +} \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..891c617 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..47729be --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + # Maintain dependencies for pip + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..4fe0fe8 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: "Lint" + +on: + push: + paths-ignore: + - "**/README.md" + - "**/info.md" + - "**/CHANGELOG.md" + - "**/CONTRIBUTING.md" + branches: + - "main" + pull_request: + branches: + - "main" + +jobs: + ruff: + name: "Ruff" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout the repository" + uses: "actions/checkout@v4.1.1" + + - name: "Set up Python" + uses: actions/setup-python@v5.0.0 + with: + python-version: "3.11" + cache: "pip" + + - name: "Install requirements" + run: python3 -m pip install -r requirements.txt + + - name: "Run" + run: python3 -m ruff check . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b692c02 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,46 @@ +name: "Release" + +on: + release: + types: + - "published" + +permissions: {} + +jobs: + release: + name: "Release" + runs-on: "ubuntu-latest" + permissions: + contents: write + steps: + - name: "Checkout the repository" + uses: "actions/checkout@v4.1.1" + + - name: "Adjust version number" + shell: "bash" + run: | + yq -i -o json '.version="${{ github.event.release.tag_name }}"' \ + "${{ github.workspace }}/custom_components/affalddk/manifest.json" + + - name: "ZIP the integration directory" + shell: "bash" + run: | + cd "${{ github.workspace }}/custom_components/affalddk" + zip affalddk.zip -r ./ + + - name: "Upload the ZIP file to the release" + uses: softprops/action-gh-release@v0.1.15 + with: + files: ${{ github.workspace }}/custom_components/affalddk/affalddk.zip + + - name: "ZIP the Entity Picture Images" + shell: "bash" + run: | + cd "${{ github.workspace }}/images/affalddk" + zip affalddk_images.zip -r ./ + + - name: "Upload the images ZIP file to the release" + uses: softprops/action-gh-release@v0.1.15 + with: + files: ${{ github.workspace }}/images/affalddk/affalddk_images.zip diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 0000000..6694c9e --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,41 @@ +name: "Validate" + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + push: + paths-ignore: + - "**/README.md" + - "**/info.md" + - "**/CHANGELOG.md" + - "**/CONTRIBUTING.md" + - "**/custom_components/affalddk/manifest.json" + branches: + - "main" + pull_request: + branches: + - "main" + +jobs: + hassfest: # https://developers.home-assistant.io/blog/2020/04/16/hassfest + name: "Hassfest Validation" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout the repository" + uses: "actions/checkout@v4.1.1" + + - name: "Run hassfest validation" + uses: "home-assistant/actions/hassfest@master" + + # hacs: # https://github.com/hacs/action + # name: "HACS Validation" + # runs-on: "ubuntu-latest" + # steps: + # - name: "Checkout the repository" + # uses: "actions/checkout@v4.1.1" + + # - name: "Run HACS validation" + # uses: "hacs/action@main" + # with: + # category: "integration" diff --git a/.gitignore b/.gitignore index 68bc17f..a5aed56 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ parts/ sdist/ var/ wheels/ +pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg @@ -49,7 +50,6 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ -cover/ # Translations *.mo @@ -72,7 +72,6 @@ instance/ docs/_build/ # PyBuilder -.pybuilder/ target/ # Jupyter Notebook @@ -83,9 +82,7 @@ profile_default/ ipython_config.py # pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -94,22 +91,7 @@ ipython_config.py # install all needed dependencies. #Pipfile.lock -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +# PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff @@ -145,16 +127,6 @@ dmypy.json # Pyre type checker .pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +renoweb.code-workspace +.DS_Store +/config/* \ No newline at end of file diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..879a9d9 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,49 @@ +# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml + +target-version = "py312" + +[lint] +select = [ + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "C", # complexity + "D", # docstrings + "E", # pycodestyle + "F", # pyflakes/autoflake + "ICN001", # import concentions; {name} should be imported as {asname} + "PGH004", # Use specific rule codes when using noqa + "PLC0414", # Useless import alias. Import alias does not rename original package. + "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass + "SIM117", # Merge with-statements that use the same scope + "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() + "SIM201", # Use {left} != {right} instead of not {left} == {right} + "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} + "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. + "SIM401", # Use get from dict with default instead of an if block + "T20", # flake8-print + "TRY004", # Prefer TypeError exception for invalid type + "RUF006", # Store a reference to the return value of asyncio.create_task + "UP", # pyupgrade + "W", # pycodestyle +] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D404", # First word of the docstring should not be This + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D411", # Missing blank line before section + "E501", # line too long + "E731", # do not assign a lambda expression, use a def +] + +[lint.flake8-pytest-style] +fixture-parentheses = false + +[lint.pyupgrade] +keep-runtime-typing = true + +[lint.mccabe] +max-complexity = 25 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4173571 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Debug Renoweb", + "type": "python", + "request": "attach", + "justMyCode": false, + "port": 5678, + "host": "localhost", + "pathMappings": [ + { + "localRoot": "/workspaces/renoweb", + "remoteRoot": "." + } + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3aa1c50 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,11 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run Home Assistant on port 8123", + "type": "shell", + "command": "scripts/develop", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2c3604c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,166 @@ +# Changelog for Renoweb Home Assistant Integration + + + ## Version 2.0.0-Beta-2 + + **Date**: `2024-03-03` + + ### Changes + + **Please see the release notes for Beta1 before installing** + +I have now been through all Municipalities and checked if they work with this Integration. There are 47 Munipalities that will work , and if you don't see your municipality in the Dropdown List, then it will not work. + +As there are new sensors added you will also have to redownload the `renoweb_images.zip` file from this release, and copy the content to your `www/renoweb` folder again. + +* Added new function to support Municipalities that only supply weekdays. (Albertslund, Furesø). +* Added new garbage type `plastmadkarton` which holds *Plast & Mad-Drikkekartoner* +* Added new garbage type `plastmetalmadmdk` which holds *Plast, Metal, Mad & Drikkekartoner* +* Added new garbage type `pappapir` which holds *Pap & Papir* +* Added new garbage type `tekstil` which holds *Tekstilaffald* +* Added new garbage type `glasplast` which holds *Glas, Plast & Madkartoner* +* Added new garbage type `plastmetalpapir` which holds *Plast, Metal & Papir* +* Fixed bug when Type String could be in more than pickup type. Happens when partial strings are the same. +* Removed the following Municipalities as they are not supported: + * Balleup + * Billund + * Fanø + * Favrskov + * Fredericia + * Frederikshavn + * Guldborgsund + * Haderslev + * Herning + * Holbæk + * Holstebro + * Ikast-Brande + * Ishøj - They use the API, but do not supply dates, only textual descriptions, which cannot be converted to dates. + * Kalundborg + * Kolding + * Læsø + * Lolland + * Middelfart + * Morsø + * Norddjurs + * Nordfyns + * Nyborg + * Odder + * Odense + * Silkeborg + * Skanderborg + * Skive + * Struer + * Syddjurs + * Thisted + * Vejle + * Vesthimmerland + * Viborg + * Tønder - They use the API in a Non-Standard way. Still under investigation if I can retrieve the data + * Vallensbæk + + + ## Version 2.0.0-Beta-1 + + **Date**: `2024-03-02` + + ### Changes + + **BREAKING CHANGE**: This is a complete rewrite of the V1.x Integration. There is no code left from the previous version, and as a result of that, there is NO DIRECT UPGRADE from version 1.x to V2.0. + + I have also used the opportunity to ensure this Integration delivers on the latest Home Assistant Requirements. + + The major changes are: + - I now use a new API. The V1 API was based on a Renoweb API that is being phased ot, and over the last few months I have seen more and more municipalities disappearing from the supported municipalities. The new API is the same most Municipalities use, when you go to their official web page and search for your address and then get Pickup Schedules. + - The `Sensors` are new, and not named the same way as the V1 sensors. Thus there is no upgrade path. With each sensor I now also iclude the official Pictograms as Entity Pictures, which you can use in your dashboard. **Note**: This image files must be installed manually - please see the README file). + - There is a new local `Calendar` entity created, which has a full-day event every time there is a Pick-up. The event will contain a Description and what content is being picked up. + - The `Binary Sensors` have not been created. If anyone uses these, raise an issue on Github. + - **BREAKING**: As statet above all sensors will get new names and new unique ID's so you will have to change all your automations, scripts and dashboards to use the new sensor names. I am sorry for that, but it could not be avoided. + + +#### UPGRADING FROM VERSION 1.X + +Here is the suggested *"Upgrade"* Procedure: + +##### Remove your current Renoweb setup +1. Go to *Settings* | *Devices & Services +2. Click on *Renoweb Garbage Collection* +3. Click on the 3 dots to the right of each address you installed, and click *Delete* + + +##### Add Renoweb V2.0 to your system +**This only applies while we are running the Beta, after that just you the normal Installation/ +Upgrade procedures** + +* Go to *HACS* and then click on *Integrations* +* Find *Renoweb Garbage Collection* and click on it. +* In the upper right corner, click on the 3 dots, and select *Redownload* +* Now, **very important**, toggle the switch, *Show beta version* to On. +* The system will think a bit, and should then contain a list with Beta and Released version. +* Find the latest Beta version and click *Download* +* Once completed, restart Home Assistant +* When the system comes back: + * Go to Configuration and Integrations + * Click the + ADD INTEGRATION button in the lower right corner. + * Search for *Renoweb** and click the integration. + +------------- + +
+ VERSION 1.x Changes + + ## Version 1.0.1 + + **Date**: `2024-01-08` + + ### Changes + - Adding a workaround for addresses and houses numbers with multile houses. (Like 2, 2A, "b etc.). You can now type the house number as , and it will get the correct address. In order to to get the ID you can use the [renoweb.py](https://github.com/briis/pyrenoweb) program and follow the instructions below: + + To get the ID number you can execute the following commands using the renoweb.py program: + + Get the Municipality ID: `python3 renoweb.py municipality` Pick your ID from the list + Get the Road ID: `python3 renoweb.py road ` + Get the Address ID (This is the ID used above): `python3 renoweb.py address `` + + Now pick the right ID from the last list of houses. + + Or if you can't get this to work, send me your address and I will find it for you 😀 + + + ## Version 1.0.0 + + - `ADDED`: For each Bin there will now be a binary_sensor called `binary_sensor.BIN_NAME_valid`. This sensor will show if data for this specific bin is valid. I use it personally with the conditional card, to only show a card if the data is valid. + - `CHANGED`: I have now rewritten some of the function to try and create more automatic recovery, should the sensor not get data on start on after an update. It will keep trying for a while, but if it takes too long it will give up, and not try again before the next timed update (Which per default is 6 hours). I did this a while ago and I do believe this introduces a **Breaking Change** as the sensors will get new names. (I honestly can't remember if this was the case) If this happens, just delete the Integration and re-add it, and then update you cards and automations with the new names. Sorry for any inconvinience. + + ## Version 0.1.16 + + - `FIXED`: Ensuring all Unit of Measurrement are always the same (dage). This ensures that the sensors can be used with Helpers like the Min/Max helper. + - `ADDED`: Added new sensor called `sensor.renoweb_days_until_next_pickup`, which shows the number of days until the next pick-up of any of the containers. + + ## Version 0.1.15 + + - `FIXED`: Fixing deprecated `async_get_registry` that might start showing up in HA 2022.6 + + ## Version 0.1.14 + + * `FIXED`: Fixes issue #10, with a deprecation warning about `device_state_attributes`. + + ## Version 0.1.13 + + * `FIXED`: **BREAKING CHANGE** Det viser sig at i nogle kommuner vil der forekomme afhentninger der hedder det samme - eksempelvis Haveaffald - men forekommer på forskellige tidspunkter. Hvis såddane forkommer, så ville kun den sidste af disse blive registreret. Denne version løser dette problem, ved at tilføje et unikt id til navnet. Men ved at gøre dette, så bryder det med tidligere versioner, som kun genererede et unikt id baseret på type. Så når man har opdateret til denne version, er det nødvendigt at: + * Slette integration, fra *Integrations* siden og derefter tilføje den igen. + * Rette på de sider hvor man viser sensorerne da de nu, for de flestes vedkommende, har fået nye navne + * Rette i eventuelle automatiseringer, som anvender disse sensorer, af samme årsag som ovenfor. + + Beklager dette, men det er den eneste måde at sikre at alle data vises for alle. + Fixer issue #7 + * `FIXED`: Tilføjet **iot_class** til `manifest.json`, som krævet af Home Assistant fra version 2021.5 + + ## Version 0.1.12-Beta + + * `FIXED`: BREAKING CHANGE Det viser sig at i nogle kommuner vil der forekomme afhentninger der hedder det samme - eksempelvis Haveaffald - men forekommer på forskellige tidspunkter. Hvis såddane forkommer, så ville kun den sidste af disse blive registreret. Denne version løser dette problem, ved at tilføje et unikt id til navnet. Men ved at gøre dette, så bryder det med tidligere versioner, som kun genererede et unikt id baseret på type. Så når man har opdateret til denne version, er det nødvendigt at: + * Slette integration, fra *Integrations* siden og derefter tilføje den igen. + * Rette på de sider hvor man viser sensorerne da de nu, for de flestes vedkommende, har fået nye navne + * Rette i eventuelle automatiseringer, som anvender disse sensorer, af samme årsag som ovenfor. + + Beklager dette, men det er den eneste måde at sikre at alle data vises for alle. +
\ No newline at end of file diff --git a/LICENSE b/LICENSE index 3b5cf1e..e7520b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Bjarne Riis +Copyright (c) 2020 Bjarne Riis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3025bf8..80f8164 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,157 @@ -# affalddk -Garbage Collection DK +# Affaldshåndtering DK + +[![GitHub Release][releases-shield]][releases] +[![GitHub Activity][commits-shield]][commits] +[![License][license-shield]](LICENSE) +[![hacs][hacsbadge]][hacs] +![Project Maintenance][maintenance-shield] +[![Community Forum][forum-shield]][forum] + + +The renoweb integration adds support for retreiving Garbage Collection data from Municipalities around Denmark. + +Municipalities in Denmark, do not have one standard for how to expose the Pickup Calendars for their citizens, and different Municipalities have different solutions. This integration currently supports the Municipalities that uses the solution from **Renoweb** and that accounts for close to 50% of all Municipalities. Go to the [Municipality List](#MUNICIPALITIES) to see if your Municipality will work with this integration. + +The biggest issue is, that there is NO standard for the way municipalities mix the content of containers. Some have glas & metal in one container, others have glas and paper in one container, etc and also even though they do mix the same content in a container, they do not name it the same. In order to have some structure I need them grouped together and this is a bit of a challenge with all these different types. If a new pickup-type is found, the system will log a warning, which you can put in an issue and I will add it to the list. Please enable logging for the wrapper module in Home assistant to get this warning in Home Assistant, by adding this code to your `configuration.yaml`: + +```yaml +logger: + default: warning + logs: + custom_components.renoweb: error + pyrenoweb: error +``` + +### This integration will set up the following platforms. + +Platform | Description +-- | -- +`sensor` | A Home Assistant `sensor` entity, with all available sensor from the API. State value will be the days until next pick-up +`calendar` | An entry will be made in to a local Home Assistant `calendar`. There will be a full-day event every time there is a pick-up, describing what is collected. + +## CREDITS + +A big thank you to @thomaspalmdk for finding the new API, and help test this new version. + +## PRE-WORK + +This integration uses the `entity_picture` attribute, which means you can get nice looking Pictograms instead of Icons on your dashboard. If you want to use this feature, do the following: +* Download the file `affalddk_images.zip` from the [latest relase](https://github.com/briis/affalddk/releases) and unzip the content. (Find the **Assets** link in the bottom of the release and click it to unfold) You should see a file called `affalddk_images` with a bunch of `.svg` files in it. +* Open a file share to the `config` share on your Home Assistant entity, and go to the `www` directory. If this directory does not exist, just create it. +* Now create a folder called `affalddk` in the `www` directory and copy all the `.svg` files to this directory. This is where this integration will look for the Entity Pictures. + +## INSTALLATION + +### HACS Installation + +This Integration is not yet part of the default HACS store, but you can add it as a Custom repository in HACS by doing the following: + +1. Go to HACS in your HA installation, and click on *Integrations* +2. Click the three vertical dots in the upper right corner, and select *Custom repositories* +3. Add `https://github.com/briis/affalddk` and select *Integration* as Category, and then click *Add* + +You should now be able to find this Integration in HACS. After the installation of the files, you must restart Home Assistant, or else you will not be able to add Affald-DK from the Integration Page. + +If you are not familiar with HACS, or haven't installed it, I would recommend to [look through the HACS documentation](https://hacs.xyz/), before continuing. Even though you can install the Integration manually, I would recommend using HACS, as you would always be reminded when a new release is published. + +### Manual Installation + +1. Using the 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 `affalddk`. +4. Download _all_ the files from the `custom_components/affalddk/` directory (folder) in this repository. +5. Place the files you downloaded in the new directory (folder) you created. +6. Restart Home Assistant +7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Affald*" + +## CONFIGURATION + +To add Affald-DK to your installation, do the following: + +- Go to Configuration and Integrations +- Click the + ADD INTEGRATION button in the lower right corner. +- Search for *Affald* and click the integration. +- When loaded, there will be a configuration box, where you must enter: + + | Parameter | Required | Default Value | Description | + | --------- | -------- | ------------- | ----------- | + | `Municipality` | Yes | None | Select your Municipality from the Dropdown list. You can press the first letter of your municipality to quickly scroll down. | + | `Road name` | Yes | None | Type the name of the road you want to get collection data for. Without house number. | + | `House Number` | Yes | None | The house number of the address. Also accepts letters. If you have a house number like 2A or similar, and it does not work, try putting a space between the number and the letter, like 2 A | + +- Click on SUBMIT to save your data. If all goes well you should now have see entities under the *Affaldshåndtering DK* integration + + +You can configure more than 1 instance of the Integration by using a different Address. + + +## MUNICIPALITIES + +Here is the list of currently supported Municipalities + + - Aabenraa + - Aalborg + - Albertslund + - Allerød + - Brøndby + - Brønderslev + - Dragør + - Egedal + - Esbjerg + - Faxe + - Fredensborg + - Frederiksberg + - Frederikssund + - Furesø + - Gentofte + - Gladsaxe + - Glostrup + - Greve + - Gribskov + - Halsnæs + - Hedensted + - Helsingør + - Herlev + - Hillerød + - Hjørring + - Horsens + - Hvidovre + - Høje-Taastrup + - Hørsholm + - Jammerbugt + - Kerteminde + - Køge + - Lyngby-Taarbæk + - Mariagerfjord + - Næstved + - Odsherred + - Randers + - Rebild + - Ringkøbing-Skjern + - Ringsted + - Roskilde + - Rødovre + - Samsø + - Slagelse + - Solrød + - Sorø + - Stevns + - Svendborg + - Sønderborg + - Tårnby + - Varde + - Vejen + - Vordingborg + +*** + +[commits-shield]: https://img.shields.io/github/commit-activity/y/briis/renoweb.svg?style=flat-square +[commits]: https://github.com/briis/renoweb/commits/main +[hacs]: https://github.com/hacs/integration +[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=flat-square +[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=flat-square +[forum]: https://community.home-assistant.io/ +[license-shield]: https://img.shields.io/github/license/briis/renoweb.svg?style=flat-square +[maintenance-shield]: https://img.shields.io/badge/maintainer-Bjarne%20Riis%20%40briis-blue.svg?style=flat-square +[releases-shield]: https://img.shields.io/github/release/briis/renoweb.svg?include_prereleases&style=flat-square&style=flat-square +[releases]: https://github.com/briis/renoweb/releases \ No newline at end of file diff --git a/affalddk.code-workspace b/affalddk.code-workspace new file mode 100644 index 0000000..362d7c2 --- /dev/null +++ b/affalddk.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": "." + } + ] +} \ No newline at end of file diff --git a/custom_components/affalddk/__init__.py b/custom_components/affalddk/__init__.py new file mode 100644 index 0000000..7270796 --- /dev/null +++ b/custom_components/affalddk/__init__.py @@ -0,0 +1,135 @@ +"""Support for the Affald DK Garbage Collection Service.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from types import MappingProxyType +from typing import Any, Self + +from pyrenoweb import ( + GarbageCollection, + PickupEvents, + RenowWebNotSupportedError, + RenowWebNotValidAddressError, + RenowWebNoConnection, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_ADDRESS_ID, + CONF_MUNICIPALITY, + CONF_UPDATE_INTERVAL, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) + +PLATFORMS = [Platform.SENSOR, Platform.CALENDAR] + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up AffaldDK from a config entry.""" + + coordinator = AffaldDKtDataUpdateCoordinator(hass, config_entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + config_entry.async_on_unload(config_entry.add_update_listener(async_update_entry)) + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + +async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Reload WeatherFlow Forecast component when options changed.""" + await hass.config_entries.async_reload(config_entry.entry_id) + +class CannotConnect(HomeAssistantError): + """Unable to connect to the web site.""" + +class AffaldDKtDataUpdateCoordinator(DataUpdateCoordinator["AffaldDKData"]): + """Class to manage fetching AffaldDK data.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Initialize global WeatherFlow forecast data updater.""" + self.affalddk = AffaldDKData( + hass, config_entry.data) + self.affalddk.initialize_data() + self.hass = hass + self.config_entry = config_entry + + update_interval = timedelta(hours=self.config_entry.options.get(CONF_UPDATE_INTERVAL, DEFAULT_SCAN_INTERVAL)) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self) -> AffaldDKData: + """Fetch data from WeatherFlow Forecast.""" + try: + return await self.affalddk.fetch_data() + except Exception as err: + raise UpdateFailed(f"Update failed: {err}") from err + + +class AffaldDKData: + """Keep data for AffaldDK.""" + + def __init__(self, hass: HomeAssistant, config: MappingProxyType[str, Any]) -> None: + """Initialise affalddk entity data.""" + + self.hass = hass + self._config = config + self.affalddk_data: GarbageCollection + self.pickup_events: PickupEvents = [] + + def initialize_data(self) -> bool: + """Establish connection to API.""" + self.affalddk_data = GarbageCollection( + municipality=self._config[CONF_MUNICIPALITY], + session=async_get_clientsession(self.hass), + ) + + return True + + async def fetch_data(self) -> Self: + """Fetch data from API.""" + + try: + resp: PickupEvents = await self.affalddk_data.get_pickup_data(address_id=self._config[CONF_ADDRESS_ID]) + except RenowWebNotSupportedError as err: + _LOGGER.debug(err) + return False + except RenowWebNotValidAddressError as err: + _LOGGER.debug(err) + return False + except RenowWebNoConnection as notreadyerror: + _LOGGER.debug(notreadyerror) + raise ConfigEntryNotReady from notreadyerror + except Exception as notreadyerror: + _LOGGER.debug(notreadyerror) + raise ConfigEntryNotReady from notreadyerror + + if not resp: + raise CannotConnect() + + self.pickup_events = resp + + return self diff --git a/custom_components/affalddk/calendar.py b/custom_components/affalddk/calendar.py new file mode 100644 index 0000000..18af9b6 --- /dev/null +++ b/custom_components/affalddk/calendar.py @@ -0,0 +1,114 @@ +"""Support for AffaldDK Waste calendars.""" +from __future__ import annotations + +import datetime +from datetime import datetime as dt, timedelta +import logging +from types import MappingProxyType +from typing import Any + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.util import dt as dt_util + +from pyrenoweb import NAME_LIST, PickupType +from . import AffaldDKtDataUpdateCoordinator +from .const import ( + CONF_ADDRESS_ID, + CONF_HOUSE_NUMBER, + CONF_ROAD_NAME, + DEFAULT_ATTRIBUTION, + DEFAULT_BRAND, + DOMAIN, +) + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up AffaldDK Waste calendard items based on a config entry.""" + + coordinator: AffaldDKtDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + if coordinator.data.pickup_events == {}: + return + + async_add_entities([AffaldDKCalendar(coordinator, config_entry)]) + + +class AffaldDKCalendar(CoordinatorEntity[DataUpdateCoordinator], CalendarEntity): + """Define a AffaldDK Waste calendar.""" + + _attr_attribution = DEFAULT_ATTRIBUTION + _attr_has_entity_name = True + _attr_name = None + + def __init__( + self, + coordinator: AffaldDKtDataUpdateCoordinator, + config: MappingProxyType[str, Any] + ) -> None: + """Initialize a AffaldDK sensor.""" + super().__init__(coordinator) + self._config = config + self._coordinator = coordinator + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._config.data[CONF_ADDRESS_ID])}, + entry_type=DeviceEntryType.SERVICE, + manufacturer=DEFAULT_BRAND, + name=f"{DOMAIN.capitalize()} {self._config.data[CONF_ROAD_NAME]} {self._config.data[CONF_HOUSE_NUMBER]}", + configuration_url="https://github.com/briis/affalddk", + ) + + self._attr_unique_id = config.data[CONF_ADDRESS_ID] + self._event: CalendarEvent | None = None + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + return self._event + + async def async_get_events(self, hass: HomeAssistant, start_date: datetime.datetime, end_date: datetime.datetime) -> list[CalendarEvent]: + """"Return calendar events within a datetime range.""" + + events = [] + for item in self._coordinator.data.pickup_events: + if self._coordinator.data.pickup_events.get(item) is None: + continue + if item == 'next_pickup': + continue + + _pickup_events: PickupType = self._coordinator.data.pickup_events.get(item) if self._coordinator.data.pickup_events else None + + _summary = NAME_LIST.get(item) + _start: dt = _pickup_events.date + _end: dt = _start + timedelta(days=1) + + if _start and _end: + events.append( + CalendarEvent( + summary=_summary, + description=_pickup_events.description, + start=dt_util.as_local(_start), + end=dt_util.as_local(_end), + ) + ) + return events + + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) diff --git a/custom_components/affalddk/config_flow.py b/custom_components/affalddk/config_flow.py new file mode 100644 index 0000000..2e936a6 --- /dev/null +++ b/custom_components/affalddk/config_flow.py @@ -0,0 +1,123 @@ +"""Config Flow for AffaldDK Integration.""" +from __future__ import annotations + +import logging +import voluptuous as vol +from typing import Any +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.selector import selector + +from pyrenoweb import ( + GarbageCollection, + MUNICIPALITIES_ARRAY, + RenoWebAddressInfo, + RenowWebNotSupportedError, + RenowWebNotValidAddressError, + RenowWebNoConnection, +) + +from .const import ( + CONF_ADDRESS_ID, + CONF_HOUSE_NUMBER, + CONF_MUNICIPALITY, + CONF_ROAD_NAME, + CONF_UPDATE_INTERVAL, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +class RenowebFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config Flow for AffaldDK.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow: + """Get the options flow for AffaldDK.""" + return AffaldDKOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle a flow initialized by the user.""" + + if user_input is None: + return await self._show_setup_form(user_input) + + errors = {} + session = async_create_clientsession(self.hass) + + try: + renoweb = GarbageCollection(municipality=user_input[CONF_MUNICIPALITY], session=session) + await renoweb.async_init() + address_info: RenoWebAddressInfo = await renoweb.get_address_id(street=user_input[CONF_ROAD_NAME], house_number=user_input[CONF_HOUSE_NUMBER]) + except RenowWebNotSupportedError: + errors["base"] = "municipality_not_supported" + return await self._show_setup_form(errors) + except RenowWebNotValidAddressError: + errors["base"] = "location_not_found" + return await self._show_setup_form(errors) + except RenowWebNoConnection: + errors["base"] = "connection_error" + return await self._show_setup_form(errors) + + await self.async_set_unique_id(address_info.address_id) + self._abort_if_unique_id_configured + + return self.async_create_entry( + title=f"{address_info.vejnavn} {address_info.husnr}", + data={ + CONF_MUNICIPALITY: address_info.kommunenavn, + CONF_ROAD_NAME: address_info.vejnavn, + CONF_HOUSE_NUMBER: address_info.husnr, + CONF_ADDRESS_ID: address_info.address_id, + } + ) + + async def _show_setup_form(self, errors=None): + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_MUNICIPALITY):selector({"select": {"options": MUNICIPALITIES_ARRAY}}), + vol.Required(CONF_ROAD_NAME): str, + vol.Required(CONF_HOUSE_NUMBER): str, + } + ), + errors=errors or {}, + ) + + +class AffaldDKOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a AffaldDK options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize the options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="Options for Renoweb", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_UPDATE_INTERVAL, + default=self.config_entry.options.get( + CONF_UPDATE_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): vol.All(vol.Coerce(int), vol.Range(min=3, max=24)), + } + ), + ) + + + diff --git a/custom_components/affalddk/const.py b/custom_components/affalddk/const.py new file mode 100644 index 0000000..eb403af --- /dev/null +++ b/custom_components/affalddk/const.py @@ -0,0 +1,20 @@ +"""Constants in renoweb component.""" + +ATTR_DATE_LONG = "date_long" +ATTR_DURATION = "duration" +ATTR_DESCRIPTION = "description" +ATTR_LAST_UPDATE = "last_update" + +CONF_ADDRESS = "address" +CONF_ADDRESS_ID = "address_id" +CONF_HOUSE_NUMBER = "house_number" +CONF_MUNICIPALITY = "municipality" +CONF_ROAD_NAME = "road_name" +CONF_UPDATE_INTERVAL = "update_interval" + +DEFAULT_ATTRIBUTION = "Data delivered by Renoweb" +DEFAULT_BRAND = "Affald DK" +DEFAULT_API_VERSION = "Legacy" +DEFAULT_SCAN_INTERVAL = 6 +DOMAIN = "affalddk" + diff --git a/custom_components/affalddk/manifest.json b/custom_components/affalddk/manifest.json new file mode 100644 index 0000000..0541984 --- /dev/null +++ b/custom_components/affalddk/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "affalddk", + "name": "Affaldshåndtering DK", + "codeowners": [ + "@briis" + ], + "config_flow": true, + "documentation": "https://github.com/briis/affalddk", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/briis/affald/issues", + "requirements": [ + "pyrenoweb==2.0.4" + ], + "version": "2.0.0-beta2" +} \ No newline at end of file diff --git a/custom_components/affalddk/sensor.py b/custom_components/affalddk/sensor.py new file mode 100644 index 0000000..b6868b5 --- /dev/null +++ b/custom_components/affalddk/sensor.py @@ -0,0 +1,285 @@ +"""Support for AffaldDK sensor data.""" +from __future__ import annotations + +import logging + +from dataclasses import dataclass +from datetime import datetime as dt +from types import MappingProxyType +from typing import Any + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.const import ATTR_DATE, ATTR_NAME, ATTR_ENTITY_PICTURE +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import AffaldDKtDataUpdateCoordinator +from .const import ( + ATTR_DATE_LONG, + ATTR_DESCRIPTION, + ATTR_DURATION, + ATTR_LAST_UPDATE, + CONF_ADDRESS_ID, + CONF_HOUSE_NUMBER, + CONF_ROAD_NAME, + DEFAULT_ATTRIBUTION, + DEFAULT_BRAND, + DOMAIN, +) +from pyrenoweb import ICON_LIST, PickupType + +@dataclass +class AffaldDKSensorEntityDescription(SensorEntityDescription): + """Describes AffaldDK sensor entity.""" + +SENSOR_TYPES: tuple[AffaldDKSensorEntityDescription, ...] = ( + AffaldDKSensorEntityDescription( + key="restaffaldmadaffald", + name="Rest- & madaffald", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="glas", + name="Glas", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="dagrenovation", + name="Dagrenovations", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="metalglas", + name="Metal & Glas", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="papirglas", + name="Papir, Pap & Glas", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="pappi", + name="Papir & Plast", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="farligtaffald", + name="Farligt affald", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="farligtaffaldmiljoboks", + name="Farligt affald & Miljøboks", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="flis", + name="Flis", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="genbrug", + name="Genbrug", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="jern", + name="Jern", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="papir", + name="Papir", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="papirmetal", + name="Papir & Metal", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="pap", + name="Pap", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="plastmetal", + name="Plast & Metal", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="storskrald", + name="Storskrald", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="storskraldogtekstilaffald", + name="Storskrald & Tekstilaffald", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="haveaffald", + name="Haveaffald", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="pappapirglasmetal", + name="Pap, Papir, Glas & Metal", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="plastmetalmadmdk", + name="Plast, Metal, Mad & Drikkekartoner", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="plastmadkarton", + name="Plast & Mad-Drikkekartoner", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="pappapir", + name="Pap & Papir", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="tekstil", + name="Tekstiler", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="glasplast", + name="Glas & Plast", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="plastmetalpapir", + name="Plast, Metal & Papir", + native_unit_of_measurement="dage", + ), + AffaldDKSensorEntityDescription( + key="next_pickup", + name="Næste afhentning", + native_unit_of_measurement="dage", + ), +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None: + """AffaldDK sensor platform.""" + coordinator: AffaldDKtDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + if coordinator.data.pickup_events == {}: + return + + + entities: list[AffaldDKSensor[Any]] = [ + AffaldDKSensor(coordinator, description, config_entry) + for description in SENSOR_TYPES if coordinator.data.pickup_events.get(description.key) is not None + ] + + async_add_entities(entities, False) + +class AffaldDKSensor(CoordinatorEntity[DataUpdateCoordinator], SensorEntity): + """A AffaldDK sensor.""" + + entity_description: AffaldDKSensorEntityDescription + _attr_attribution = DEFAULT_ATTRIBUTION + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AffaldDKtDataUpdateCoordinator, + description: AffaldDKSensorEntityDescription, + config: MappingProxyType[str, Any] + ) -> None: + """Initialize a AffaldDK sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._config = config + self._coordinator = coordinator + self._pickup_events: PickupType = coordinator.data.pickup_events.get(description.key) if coordinator.data.pickup_events else None + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._config.data[CONF_ADDRESS_ID])}, + entry_type=DeviceEntryType.SERVICE, + manufacturer=DEFAULT_BRAND, + name=f"{DOMAIN.capitalize()} {self._config.data[CONF_ROAD_NAME]} {self._config.data[CONF_HOUSE_NUMBER]}", + configuration_url="https://github.com/briis/affalddk", + ) + self._attr_unique_id = f"{config.data[CONF_ADDRESS_ID]} {description.key}" + + @property + def native_unit_of_measurement(self) -> str | None: + """Return unit of sensor.""" + + current_time = dt.today() + pickup_time: dt = self._pickup_events.date + if pickup_time: + if ((pickup_time - current_time).days + 1) == 1: + return "dag" + + return super().native_unit_of_measurement + + @property + def native_value(self) -> StateType: + """Return state of the sensor.""" + + current_time = dt.today() + pickup_time: dt = self._pickup_events.date + if pickup_time: + return (pickup_time - current_time).days + 1 + + @property + def icon(self) -> str | None: + """Return icon for sensor.""" + + return ICON_LIST.get(self.entity_description.key) + + @property + def extra_state_attributes(self) -> None: + """Return non standard attributes.""" + + _date: dt = self._pickup_events.date + _current_date = dt.today() + _state = (_date - _current_date).days + 1 + _day_number = _date.weekday() + _weekdays = ["Man", "Tir", "Ons", "Tor", "Fre", "Lør", "Søn"] + _weekdays_full = ["Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag", "Søndag"] + _day_name = _weekdays[_day_number] + _day_name_long = _weekdays_full[_day_number] + if _state == 0: + _day_text = "I dag" + elif _state == 1: + _day_text = "I morgen" + else: + _day_text = f"Om {_state} dage" + + return { + ATTR_DATE: _date.date() if _date else None, + ATTR_DATE_LONG: f"{_day_name_long} {_date.strftime("d. %d-%m-%Y") if _date else None}" , + ATTR_DESCRIPTION: self._pickup_events.description, + ATTR_DURATION: _day_text, + ATTR_ENTITY_PICTURE: f"/local/affalddk/{self._pickup_events.entity_picture}?{str(_current_date.timestamp())}", + ATTR_LAST_UPDATE: self._pickup_events.last_updated, + ATTR_NAME: self._pickup_events.friendly_name, + } + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) + diff --git a/custom_components/affalddk/translations/da.json b/custom_components/affalddk/translations/da.json new file mode 100644 index 0000000..e82d235 --- /dev/null +++ b/custom_components/affalddk/translations/da.json @@ -0,0 +1,35 @@ +{ + "config": { + "error": { + "name_exists": "Adressen er allerede sat op. Brug en anden adresse.", + "request_error": "Fejl ved hentning af data fra serveren.", + "location_not_found": "Den angivne adresse blev ikke fundet.", + "municipality_not_supported": "Kommunen er ikke understøttet i denne Integration", + "connection_error": "Renoweb svarer ikke. Prøv igen senere." + }, + "step": { + "user": { + "data": { + "municipality": "Vælg Kommune", + "road_name": "Vej navn", + "house_number": "Hus nummer", + "update_interval": "Opdaterings interval (timer)" + }, + "description": "Hent opsamlingsdata fra danske kommuner.", + "title": "Affald DK" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "municipality": "Vælg Kommune", + "road_name": "Vej navn", + "house_number": "Hus nummer", + "update_interval": "Opdaterings interval (timer)" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/affalddk/translations/en.json b/custom_components/affalddk/translations/en.json new file mode 100644 index 0000000..c306a59 --- /dev/null +++ b/custom_components/affalddk/translations/en.json @@ -0,0 +1,35 @@ +{ + "config": { + "error": { + "name_exists": "Address already setup. Choose another address.", + "location_not_found": "The location you entered is not found", + "request_error": "Error retrieving data from the Endpoint", + "municipality_not_supported": "The Municipality is not supported by this Integration", + "connection_error": "Renoweb is not sending data. Please try again later." + }, + "step": { + "user": { + "data": { + "municipality": "Select Municipality", + "road_name": "Road name", + "house_number": "House Number", + "update_interval": "Update Interval (Hours)" + }, + "description": "Setup Home Assistant to collect Pick-Up data from Danish Municipalities.", + "title": "Affald DK" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "municipality": "Select Municipality", + "road_name": "Road name", + "house_number": "House Number", + "update_interval": "Update Interval (Hours)" + } + } + } + } +} \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..172af8f --- /dev/null +++ b/hacs.json @@ -0,0 +1,6 @@ +{ + "name": "Affaldshåndtering DK", + "content_in_root": false, + "render_readme": true, + "homeassistant": "2023.6.0" +} \ No newline at end of file diff --git a/images/affalddk/dagrenovation.svg b/images/affalddk/dagrenovation.svg new file mode 100644 index 0000000..f1a1ae7 --- /dev/null +++ b/images/affalddk/dagrenovation.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/affalddk/farligtaffald.svg b/images/affalddk/farligtaffald.svg new file mode 100644 index 0000000..56969ec --- /dev/null +++ b/images/affalddk/farligtaffald.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/affalddk/farligtaffaldmiljoboks.svg b/images/affalddk/farligtaffaldmiljoboks.svg new file mode 100644 index 0000000..56969ec --- /dev/null +++ b/images/affalddk/farligtaffaldmiljoboks.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/affalddk/flis.svg b/images/affalddk/flis.svg new file mode 100644 index 0000000..a48d12b --- /dev/null +++ b/images/affalddk/flis.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/affalddk/genbrug.svg b/images/affalddk/genbrug.svg new file mode 100644 index 0000000..19d1fa0 --- /dev/null +++ b/images/affalddk/genbrug.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/affalddk/glas.svg b/images/affalddk/glas.svg new file mode 100644 index 0000000..4b6b960 --- /dev/null +++ b/images/affalddk/glas.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/affalddk/glasplast.svg b/images/affalddk/glasplast.svg new file mode 100644 index 0000000..a4174c5 --- /dev/null +++ b/images/affalddk/glasplast.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/images/affalddk/haveaffald.svg b/images/affalddk/haveaffald.svg new file mode 100644 index 0000000..a48d12b --- /dev/null +++ b/images/affalddk/haveaffald.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/affalddk/jern.svg b/images/affalddk/jern.svg new file mode 100644 index 0000000..9cc3fa9 --- /dev/null +++ b/images/affalddk/jern.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/affalddk/metalglas.svg b/images/affalddk/metalglas.svg new file mode 100644 index 0000000..2aa003a --- /dev/null +++ b/images/affalddk/metalglas.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/affalddk/pap.svg b/images/affalddk/pap.svg new file mode 100644 index 0000000..af82b47 --- /dev/null +++ b/images/affalddk/pap.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/affalddk/papir.svg b/images/affalddk/papir.svg new file mode 100644 index 0000000..c3a9024 --- /dev/null +++ b/images/affalddk/papir.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/affalddk/papirglas.svg b/images/affalddk/papirglas.svg new file mode 100644 index 0000000..ff23df3 --- /dev/null +++ b/images/affalddk/papirglas.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/images/affalddk/papirmetal.svg b/images/affalddk/papirmetal.svg new file mode 100644 index 0000000..a98e494 --- /dev/null +++ b/images/affalddk/papirmetal.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/affalddk/pappapir.svg b/images/affalddk/pappapir.svg new file mode 100644 index 0000000..6618b7d --- /dev/null +++ b/images/affalddk/pappapir.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/images/affalddk/pappapirglasmetal.svg b/images/affalddk/pappapirglasmetal.svg new file mode 100644 index 0000000..3ddd988 --- /dev/null +++ b/images/affalddk/pappapirglasmetal.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/affalddk/pappi.svg b/images/affalddk/pappi.svg new file mode 100644 index 0000000..0ef347f --- /dev/null +++ b/images/affalddk/pappi.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/images/affalddk/plastmadkarton.svg b/images/affalddk/plastmadkarton.svg new file mode 100644 index 0000000..ad51f99 --- /dev/null +++ b/images/affalddk/plastmadkarton.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/affalddk/plastmetal.svg b/images/affalddk/plastmetal.svg new file mode 100644 index 0000000..4dd8bf1 --- /dev/null +++ b/images/affalddk/plastmetal.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/affalddk/plastmetalmadmdk.svg b/images/affalddk/plastmetalmadmdk.svg new file mode 100644 index 0000000..6ee102c --- /dev/null +++ b/images/affalddk/plastmetalmadmdk.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/images/affalddk/plastmetalpapir.svg b/images/affalddk/plastmetalpapir.svg new file mode 100644 index 0000000..f1794ef --- /dev/null +++ b/images/affalddk/plastmetalpapir.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/images/affalddk/restaffaldmadaffald.svg b/images/affalddk/restaffaldmadaffald.svg new file mode 100644 index 0000000..e38507a --- /dev/null +++ b/images/affalddk/restaffaldmadaffald.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/affalddk/storskrald.svg b/images/affalddk/storskrald.svg new file mode 100644 index 0000000..d535a7f --- /dev/null +++ b/images/affalddk/storskrald.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/affalddk/storskraldogtekstilaffald.svg b/images/affalddk/storskraldogtekstilaffald.svg new file mode 100644 index 0000000..d535a7f --- /dev/null +++ b/images/affalddk/storskraldogtekstilaffald.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/affalddk/tekstil.svg b/images/affalddk/tekstil.svg new file mode 100644 index 0000000..19d1fa0 --- /dev/null +++ b/images/affalddk/tekstil.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/brand/icon.png b/images/brand/icon.png new file mode 100644 index 0000000..9a599a6 Binary files /dev/null and b/images/brand/icon.png differ diff --git a/images/brand/icon@2x.png b/images/brand/icon@2x.png new file mode 100644 index 0000000..14f0487 Binary files /dev/null and b/images/brand/icon@2x.png differ diff --git a/images/brand/logo.png b/images/brand/logo.png new file mode 100644 index 0000000..a908d69 Binary files /dev/null and b/images/brand/logo.png differ diff --git a/images/brand/logo@2x.png b/images/brand/logo@2x.png new file mode 100644 index 0000000..d5610c6 Binary files /dev/null and b/images/brand/logo@2x.png differ diff --git a/images/documentation/config_flow.png b/images/documentation/config_flow.png new file mode 100644 index 0000000..1e155a8 Binary files /dev/null and b/images/documentation/config_flow.png differ diff --git a/images/original/dagrenovation.svg b/images/original/dagrenovation.svg new file mode 100644 index 0000000..f1a1ae7 --- /dev/null +++ b/images/original/dagrenovation.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/original/glas.svg b/images/original/glas.svg new file mode 100644 index 0000000..4b6b960 --- /dev/null +++ b/images/original/glas.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/original/mad-ogdrikkekartoner.svg b/images/original/mad-ogdrikkekartoner.svg new file mode 100644 index 0000000..9c330e0 --- /dev/null +++ b/images/original/mad-ogdrikkekartoner.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/original/madaffald.svg b/images/original/madaffald.svg new file mode 100644 index 0000000..430a5f2 --- /dev/null +++ b/images/original/madaffald.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/original/metal.svg b/images/original/metal.svg new file mode 100644 index 0000000..9cc3fa9 --- /dev/null +++ b/images/original/metal.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/original/pap.svg b/images/original/pap.svg new file mode 100644 index 0000000..af82b47 --- /dev/null +++ b/images/original/pap.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/original/papir.svg b/images/original/papir.svg new file mode 100644 index 0000000..c3a9024 --- /dev/null +++ b/images/original/papir.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/original/plast.svg b/images/original/plast.svg new file mode 100644 index 0000000..8aad386 --- /dev/null +++ b/images/original/plast.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/original/tekstiler.svg b/images/original/tekstiler.svg new file mode 100644 index 0000000..19d1fa0 --- /dev/null +++ b/images/original/tekstiler.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/pixelmator/dagrenovation.pxd b/images/pixelmator/dagrenovation.pxd new file mode 100644 index 0000000..4f86a0d Binary files /dev/null and b/images/pixelmator/dagrenovation.pxd differ diff --git a/images/pixelmator/farligtaffald.pxd b/images/pixelmator/farligtaffald.pxd new file mode 100644 index 0000000..3aa8e74 Binary files /dev/null and b/images/pixelmator/farligtaffald.pxd differ diff --git a/images/pixelmator/glas.pxd b/images/pixelmator/glas.pxd new file mode 100644 index 0000000..dba77e5 Binary files /dev/null and b/images/pixelmator/glas.pxd differ diff --git a/images/pixelmator/glasplast.pxd b/images/pixelmator/glasplast.pxd new file mode 100644 index 0000000..3843b42 Binary files /dev/null and b/images/pixelmator/glasplast.pxd differ diff --git a/images/pixelmator/haveaffald.pxd b/images/pixelmator/haveaffald.pxd new file mode 100644 index 0000000..f70506a Binary files /dev/null and b/images/pixelmator/haveaffald.pxd differ diff --git a/images/pixelmator/jern.pxd b/images/pixelmator/jern.pxd new file mode 100644 index 0000000..d510bad Binary files /dev/null and b/images/pixelmator/jern.pxd differ diff --git a/images/pixelmator/logo.pxd b/images/pixelmator/logo.pxd new file mode 100644 index 0000000..6787717 Binary files /dev/null and b/images/pixelmator/logo.pxd differ diff --git a/images/pixelmator/mad-ogdrikkekartoner.pxd b/images/pixelmator/mad-ogdrikkekartoner.pxd new file mode 100644 index 0000000..bfffb04 Binary files /dev/null and b/images/pixelmator/mad-ogdrikkekartoner.pxd differ diff --git a/images/pixelmator/metalglas.pxd b/images/pixelmator/metalglas.pxd new file mode 100644 index 0000000..b119c45 Binary files /dev/null and b/images/pixelmator/metalglas.pxd differ diff --git a/images/pixelmator/pap.pxd b/images/pixelmator/pap.pxd new file mode 100644 index 0000000..52b97ba Binary files /dev/null and b/images/pixelmator/pap.pxd differ diff --git a/images/pixelmator/papir.pxd b/images/pixelmator/papir.pxd new file mode 100644 index 0000000..a98f47b Binary files /dev/null and b/images/pixelmator/papir.pxd differ diff --git a/images/pixelmator/papir.svg b/images/pixelmator/papir.svg new file mode 100644 index 0000000..c3a9024 --- /dev/null +++ b/images/pixelmator/papir.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/pixelmator/papirglas.pxd b/images/pixelmator/papirglas.pxd new file mode 100644 index 0000000..36ad7e5 Binary files /dev/null and b/images/pixelmator/papirglas.pxd differ diff --git a/images/pixelmator/papirmetal.pxd b/images/pixelmator/papirmetal.pxd new file mode 100644 index 0000000..6858478 Binary files /dev/null and b/images/pixelmator/papirmetal.pxd differ diff --git a/images/pixelmator/pappapir.pxd b/images/pixelmator/pappapir.pxd new file mode 100644 index 0000000..38065ed Binary files /dev/null and b/images/pixelmator/pappapir.pxd differ diff --git a/images/pixelmator/pappapirglasmetal.pxd b/images/pixelmator/pappapirglasmetal.pxd new file mode 100644 index 0000000..ed122e4 Binary files /dev/null and b/images/pixelmator/pappapirglasmetal.pxd differ diff --git a/images/pixelmator/pappi.pxd b/images/pixelmator/pappi.pxd new file mode 100644 index 0000000..89d22a3 Binary files /dev/null and b/images/pixelmator/pappi.pxd differ diff --git a/images/pixelmator/plastmadkarton.pxd b/images/pixelmator/plastmadkarton.pxd new file mode 100644 index 0000000..69542ae Binary files /dev/null and b/images/pixelmator/plastmadkarton.pxd differ diff --git a/images/pixelmator/plastmetal.pxd b/images/pixelmator/plastmetal.pxd new file mode 100644 index 0000000..0cdf2c2 Binary files /dev/null and b/images/pixelmator/plastmetal.pxd differ diff --git a/images/pixelmator/plastmetalmadmdk.pxd b/images/pixelmator/plastmetalmadmdk.pxd new file mode 100644 index 0000000..f101859 Binary files /dev/null and b/images/pixelmator/plastmetalmadmdk.pxd differ diff --git a/images/pixelmator/plastmetalpapir.pxd b/images/pixelmator/plastmetalpapir.pxd new file mode 100644 index 0000000..0788757 Binary files /dev/null and b/images/pixelmator/plastmetalpapir.pxd differ diff --git a/images/pixelmator/restaffaldmadaffald.pxd b/images/pixelmator/restaffaldmadaffald.pxd new file mode 100644 index 0000000..7d71333 Binary files /dev/null and b/images/pixelmator/restaffaldmadaffald.pxd differ diff --git a/images/pixelmator/storskrald.pxd b/images/pixelmator/storskrald.pxd new file mode 100644 index 0000000..61e7824 Binary files /dev/null and b/images/pixelmator/storskrald.pxd differ diff --git a/images/renoweb.zip b/images/renoweb.zip new file mode 100644 index 0000000..d5bd62e Binary files /dev/null and b/images/renoweb.zip differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8f67ca7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +colorlog==6.8.2 +homeassistant==2024.2.5 +pip>=23.2.1,<24.1 +ruff==0.2.2 diff --git a/scripts/dev-branch b/scripts/dev-branch new file mode 100755 index 0000000..4022526 --- /dev/null +++ b/scripts/dev-branch @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +python3 -m pip --disable-pip-version-check install --upgrade git+https://github.com/home-assistant/home-assistant.git@dev diff --git a/scripts/develop b/scripts/develop new file mode 100755 index 0000000..20366e8 --- /dev/null +++ b/scripts/develop @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +# Create config dir if not present +if [[ ! -d "${PWD}/config" ]]; then + mkdir -p "${PWD}/config" + hass --config "${PWD}/config" --script ensure_config +fi + +# Set the path to custom_components +## This let's us have the structure we want /custom_components/integration_blueprint +## while at the same time have Home Assistant configuration inside /config +## without resulting to symlinks. +export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" + +# Start Home Assistant +hass --config "${PWD}/config" --debug \ No newline at end of file diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..e3c4904 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +ruff check . \ No newline at end of file diff --git a/scripts/lint-fix b/scripts/lint-fix new file mode 100755 index 0000000..752d23a --- /dev/null +++ b/scripts/lint-fix @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +ruff check . --fix \ No newline at end of file diff --git a/scripts/setup b/scripts/setup new file mode 100755 index 0000000..141d19f --- /dev/null +++ b/scripts/setup @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +python3 -m pip install --requirement requirements.txt diff --git a/scripts/specific-version b/scripts/specific-version new file mode 100755 index 0000000..fbfdd6f --- /dev/null +++ b/scripts/specific-version @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +read -p 'Set Home Assistant version: ' -r version +python3 -m pip --disable-pip-version-check install --upgrade homeassistant=="$version" diff --git a/scripts/upgrade b/scripts/upgrade new file mode 100755 index 0000000..7ce8f78 --- /dev/null +++ b/scripts/upgrade @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +python3 -m pip install --upgrade --pre homeassistant diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b3b0ead --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[flake8] +ignore = E226,E302,E41, W503 +max-line-length = 100 +exclude = tests/* +max-complexity = 10