diff --git a/.devcontainer.json b/.devcontainer.json
new file mode 100644
index 0000000..2872ab9
--- /dev/null
+++ b/.devcontainer.json
@@ -0,0 +1,36 @@
+{
+ "name": "ludeeus/integration_blueprint",
+ "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.10-bullseye",
+ "postCreateCommand": "scripts/setup",
+ "forwardPorts": [
+ 8123
+ ],
+ "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,
+ "python.linting.pylintEnabled": true,
+ "python.linting.enabled": true,
+ "python.formatting.provider": "black",
+ "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
+ "editor.formatOnPaste": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "files.trimTrailingWhitespace": true
+ }
+ }
+ },
+ "remoteUser": "vscode",
+ "features": {
+ "rust": "latest"
+ }
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..94f480d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
\ No newline at end of file
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
deleted file mode 100644
index 921e613..0000000
--- a/.github/workflows/validate.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: Validate
-
-on:
- push:
- branches:
- - master
- pull_request:
- schedule:
- - cron: "0 0 * * *"
-
-jobs:
- hacs:
- runs-on: "ubuntu-latest"
- steps:
- - uses: "actions/checkout@v2"
- - name: HACS validation
- uses: "hacs/action@main"
- with:
- category: "integration"
-
- hassfest:
- runs-on: "ubuntu-latest"
- name: Hassfest
- steps:
- - name: Check out the repository
- uses: "actions/checkout@v2.3.4"
-
- - name: Hassfest validation
- uses: "home-assistant/actions/hassfest@master"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d901320..5bd46d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,5 +14,31 @@ eggs
*.orig
.idea
-# vscode
+# artifacts
+__pycache__
+.pytest*
+*.egg-info
+*/build/*
+*/dist/*
+
+
+# misc
+.coverage
.vscode
+coverage.xml
+
+
+# Home Assistant configuration
+.cloud
+.HA_VERSION
+.storage
+automations.yaml
+blueprints
+configuration.yaml
+deps
+home-assistant_v2*
+home-assistant.log*
+tts
+scenes.yaml
+scripts.yaml
+secrets.yaml
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
deleted file mode 100644
index ff94dd2..0000000
--- a/.pre-commit-config.yaml
+++ /dev/null
@@ -1,58 +0,0 @@
-repos:
- - repo: https://github.com/asottile/pyupgrade
- rev: v2.3.0
- hooks:
- - id: pyupgrade
- args: [--py37-plus]
- - repo: https://github.com/psf/black
- rev: 19.10b0
- hooks:
- - id: black
- args:
- - --safe
- - --quiet
- files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
- - repo: https://github.com/codespell-project/codespell
- rev: v1.16.0
- hooks:
- - id: codespell
- args:
- - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
- - --skip="./.*,*.csv,*.json"
- - --quiet-level=2
- exclude_types: [csv, json]
- - repo: https://gitlab.com/pycqa/flake8
- rev: 3.8.1
- hooks:
- - id: flake8
- additional_dependencies:
- - flake8-docstrings==1.5.0
- - pydocstyle==5.0.2
- files: ^(homeassistant|script|tests)/.+\.py$
- - repo: https://github.com/PyCQA/bandit
- rev: 1.6.2
- hooks:
- - id: bandit
- args:
- - --quiet
- - --format=custom
- - --configfile=tests/bandit.yaml
- files: ^(homeassistant|script|tests)/.+\.py$
- - repo: https://github.com/pre-commit/mirrors-isort
- rev: v4.3.21
- hooks:
- - id: isort
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v2.4.0
- hooks:
- - id: check-executables-have-shebangs
- stages: [manual]
- - id: check-json
- - repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.770
- hooks:
- - id: mypy
- args:
- - --pretty
- - --show-error-codes
- - --show-error-context
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..fd953bd
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,61 @@
+# Contribution guidelines
+
+Contributing to this project should be as easy and transparent as possible, whether it's:
+
+- Reporting a bug
+- Discussing the current state of the code
+- Submitting a fix
+- Proposing new features
+
+## Github is used for everything
+
+Github is used to host code, to track issues and feature requests, as well as accept pull requests.
+
+Pull requests are the best way to propose changes to the codebase.
+
+1. Fork the repo and create your branch from `master`.
+2. If you've changed something, update the documentation.
+3. Make sure your code lints (using black).
+4. Test you contribution.
+5. Issue that pull request!
+
+## Any contributions you make will be under the MIT Software License
+
+In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
+
+## Report bugs using Github's [issues](../../issues)
+
+GitHub issues are used to track public bugs.
+Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
+
+## Write bug reports with detail, background, and sample code
+
+**Great Bug Reports** tend to have:
+
+- A quick summary and/or background
+- Steps to reproduce
+ - Be specific!
+ - Give sample code if you can.
+- What you expected would happen
+- What actually happens
+- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
+
+People *love* thorough bug reports. I'm not even kidding.
+
+## Use a Consistent Coding Style
+
+Use [black](https://github.com/ambv/black) to make sure the code follows the style.
+
+## Test your code modification
+
+This custom component is based on [integration_blueprint template](https://github.com/ludeeus/integration_blueprint).
+
+It comes with development environment in a container, easy to launch
+if you use Visual Studio Code. With this container you will have a stand alone
+Home Assistant instance running and already configured with the included
+[`configuration.yaml`](./configuration.yaml)
+file.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under its MIT License.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..64cebfd
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Jon Kristian Nilsen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index b70bdbf..781852f 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,46 @@
# Wasteplan TRV
-# NOTE
-Unfortunately trv api v1 doesn't seem to work any longer, and it seems that they don't plan on giving access to v2, so this component will no longer work. [Ticket with response from trv](/issues/13).
-[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs)
+[![GitHub Release][releases-shield]][releases]
+[![License][license-shield]](LICENSE)
+[![hacs][hacsbadge]][hacs]
+![Project Maintenance][maintenance-shield]
+[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
Wasteplan component for Trondheim Renholdsverk (TRV).
-This component provides sensors for your bins or containers and gives you status about pickup.
-
-[![image-1.png](https://i.postimg.cc/hGs0gPr7/image-1.png)](https://postimg.cc/f33dfs1w)
+This component creates a calendar of pickup events.
## Installation
-### Manual installation
-Download or clone and copy the folder `custom/components/wasteplan_trv` into your `custom_components/`
-
-### Installation via Home Assistant Community Store (HACS)
1. Ensure [HACS](http://hacs.xyz/) is installed.
2. Search for and install the "Wasteplan TRV" integration
-3. Configure the sensor
-4. Restart Home Assistant
+3. Install and restart Home Assistant
+4. Install Wasteplan TRV from the integrations screen.
-## Finding your ID
+## Attributions
+- This component uses the excellent [integration_blueprint] from [ludeeus].
-To locate your ID, append your address to the end of one of the URLs below, either bin or container.
-- Bins: https://trv.no/wp-json/wasteplan/v1/bins/?s=
-- Containers: https://trv.no/wp-json/wasteplan/v1/containers/?s=
+## Contributions are welcome!
-### Configuration variables
-| Variable | Required | Type | Description |
-| -------- | ---------- | ----------- | ----------- |
-| `id` | yes | integer | Bin/Container ID. |
-| `pickup_day` | no | integer | Pickup day of the week. Defaults to 0 (Monday). |
+If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)
-## Example
-```yaml
-sensor:
- - platform: wasteplan_trv
- id: 774
- pickup_day: 0
-```
+***
⭐️ this repository if you found it useful ❤️
-
+[![BuyMeCoffee][buymecoffebadge2]][buymecoffee]
+
+[wasteplan_trv]: https://github.com/jonkristian/wasteplan_trv
+[buymecoffee]: https://www.buymeacoffee.com/jonkristian
+[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge
+[buymecoffebadge2]: https://bmc-cdn.nyc3.digitaloceanspaces.com/BMC-button-images/custom_images/white_img.png
+[hacs]: https://github.com/hacs/integration
+[hacsbadge]: https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge
+[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge
+[forum]: https://community.home-assistant.io/
+[license-shield]: https://img.shields.io/github/license/jonkristian/wasteplan_trv.svg?style=for-the-badge
+[maintenance-shield]: https://img.shields.io/badge/maintainer-Jon%20Kristian%20sNilsen%20%40jonkristian-blue.svg?style=for-the-badge
+[releases-shield]: https://img.shields.io/github/release/jonkristian/wasteplan_trv.svg?style=for-the-badge
+[releases]: https://github.com/jonkristian/wasteplan_trv/releases
+[exampleimg]: example.png
+[integration_blueprint]: https://github.com/ludeeus/integration_blueprint
+[ludeeus]: https://github.com/ludeeus/
diff --git a/custom_components/wasteplan_trv/__init__.py b/custom_components/wasteplan_trv/__init__.py
index 9f483b2..6095d5e 100644
--- a/custom_components/wasteplan_trv/__init__.py
+++ b/custom_components/wasteplan_trv/__init__.py
@@ -1 +1,103 @@
-"""Component for integrating wasteplan_trv."""
+"""Wasteplan TRV integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.entity import DeviceInfo, EntityDescription
+from homeassistant.helpers.device_registry import DeviceEntryType
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .api import (
+ TRVApiClient,
+ TRVApiClientError,
+)
+from .const import DOMAIN, LOCATION_ID, LOGGER
+
+PLATFORMS: list[Platform] = [
+ Platform.CALENDAR,
+]
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up this integration using UI."""
+ hass.data.setdefault(DOMAIN, {})
+
+ hass.data[DOMAIN][entry.entry_id] = coordinator = TRVDataUpdateCoordinator(
+ hass=hass,
+ client=TRVApiClient(
+ location_id=entry.data[LOCATION_ID],
+ address="",
+ session=async_get_clientsession(hass),
+ ),
+ )
+
+ await coordinator.async_config_entry_first_refresh()
+
+ await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+ entry.async_on_unload(entry.add_update_listener(async_reload_entry))
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Handle removal of an entry."""
+ if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
+ hass.data[DOMAIN].pop(entry.entry_id)
+ return unloaded
+
+
+async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Reload config entry."""
+ await async_unload_entry(hass, entry)
+ await async_setup_entry(hass, entry)
+
+
+class TRVDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching data from the API."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ client: TRVApiClient,
+ ) -> None:
+ """Initialize."""
+ self.client = client
+ super().__init__(
+ hass=hass,
+ logger=LOGGER,
+ name=DOMAIN,
+ update_interval=timedelta(hours=5),
+ )
+ self.entities: list[TRVEntity] = []
+
+ async def _async_update_data(self):
+ """Update data via library."""
+ try:
+ return await self.client.async_get_pickups()
+ except TRVApiClientError as exception:
+ raise UpdateFailed(exception) from exception
+
+
+class TRVEntity(CoordinatorEntity):
+ """Representation of a Wasteplan entity."""
+ def __init__(
+ self,
+ coordinator: TRVDataUpdateCoordinator,
+ entry: ConfigEntry,
+ ) -> None:
+ """Initialize Wasteplan entity."""
+ super().__init__(coordinator=coordinator)
+ self._attr_device_info = DeviceInfo(
+ identifiers={(DOMAIN, str(entry.data[LOCATION_ID]))},
+ entry_type=DeviceEntryType.SERVICE,
+ configuration_url="https://github.com/jonkristian/wasteplan_trv",
+ manufacturer="Trondheim Renholdsverk",
+ name="Trondheim Renholdsverk",
+ )
diff --git a/custom_components/wasteplan_trv/api.py b/custom_components/wasteplan_trv/api.py
new file mode 100644
index 0000000..ec6d841
--- /dev/null
+++ b/custom_components/wasteplan_trv/api.py
@@ -0,0 +1,69 @@
+"""TRV API Client."""
+from __future__ import annotations
+
+import asyncio
+import socket
+
+import aiohttp
+import async_timeout
+
+
+class TRVApiClientError(Exception):
+ """Exception to indicate a general API error."""
+
+
+class TRVApiClientCommunicationError(TRVApiClientError):
+ """Exception to indicate a communication error."""
+
+
+class TRVApiClient:
+ """TRV API Client."""
+ def __init__(
+ self,
+ address: str,
+ location_id: str,
+ session: aiohttp.ClientSession,
+ ) -> None:
+ self._location_id = location_id
+ self._address = address
+ self._session = session
+
+ async def async_lookup_address(self) -> any:
+ """Get address locations from TRV."""
+ return await self._api_wrapper(
+ method="get",
+ url="https://trv.no/wp-json/wasteplan/v2/adress?s=" + self._address,
+ )
+
+ async def async_get_pickups(self) -> any:
+ """Get pickup base data from TRV."""
+ return await self._api_wrapper(
+ method="get",
+ url="https://trv.no/wp-json/wasteplan/v2/calendar/" + self._location_id,
+ )
+
+ async def _api_wrapper(
+ self,
+ method: str,
+ url: str,
+ data: dict | None = None,
+ headers: dict | None = None,
+ ) -> any:
+ """Fetch the information from the API."""
+ try:
+ async with async_timeout.timeout(10):
+ response = await self._session.request(
+ method=method,
+ url=url,
+ headers=headers,
+ json=data,
+ )
+ response.raise_for_status()
+ return await response.json()
+
+ except asyncio.TimeoutError as exception:
+ raise TRVApiClientCommunicationError("Timeout error fetching information") from exception
+ except (aiohttp.ClientError, socket.gaierror) as exception:
+ raise TRVApiClientCommunicationError("Error fetching information") from exception
+ except Exception as exception: # pylint: disable=broad-except
+ raise TRVApiClientError("Something really wrong happened!") from exception
diff --git a/custom_components/wasteplan_trv/calendar.py b/custom_components/wasteplan_trv/calendar.py
new file mode 100644
index 0000000..50012ba
--- /dev/null
+++ b/custom_components/wasteplan_trv/calendar.py
@@ -0,0 +1,73 @@
+"""Support for Wasteplan TRV calendar."""
+from __future__ import annotations
+import logging
+from datetime import date, datetime, timedelta
+from homeassistant.util import dt
+
+from homeassistant.components.calendar import CalendarEntity, CalendarEvent
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from . import TRVEntity
+from .const import DOMAIN, CALENDAR_NAME, LOCATION_ID
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+ """Set up Wasteplan calendars based on a config entry."""
+ coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+ async_add_entities([TRVCalendar(coordinator, entry)])
+
+
+class TRVCalendar(TRVEntity, CalendarEntity):
+ """Define a Wasteplan calendar."""
+ def __init__(
+ self,
+ coordinator: DataUpdateCoordinator,
+ entry: ConfigEntry,
+ ) -> None:
+ """Initialize the Wasteplan entity."""
+ super().__init__(coordinator, entry)
+ self._attr_unique_id = str(entry.data[LOCATION_ID])
+ self._attr_name = str(entry.data[CALENDAR_NAME])
+ 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,
+ end_date: datetime,
+ ) -> list[CalendarEvent]:
+ """Return calendar events within a datetime range."""
+ events: list[CalendarEvent] = []
+
+ # start = start_date.strftime("%Y-%m-%d")
+ # end = end_date.strftime("%Y-%m-%d")
+
+ for waste in self.coordinator.data["calendar"]:
+ waste_date = datetime.strptime(waste["dato"], "%Y-%m-%dT%H:%M:%S")
+ start = dt.start_of_local_day(waste_date)
+
+ event = CalendarEvent(
+ summary=str(waste["fraksjon"]),
+ start=start,
+ end=start + timedelta(days=1),
+ )
+
+ if event is not None:
+ events.append(event)
+
+ return events
+
+ async def get_calendar_item(self, calendar_item) -> object | None:
+ """Return formatted calendar entry."""
\ No newline at end of file
diff --git a/custom_components/wasteplan_trv/config_flow.py b/custom_components/wasteplan_trv/config_flow.py
new file mode 100644
index 0000000..bbcd9ac
--- /dev/null
+++ b/custom_components/wasteplan_trv/config_flow.py
@@ -0,0 +1,112 @@
+"""config flow for the Wasteplan TRV integration."""
+from __future__ import annotations
+
+import voluptuous as vol
+from homeassistant import config_entries
+from homeassistant.data_entry_flow import FlowResult
+from homeassistant.helpers.aiohttp_client import async_create_clientsession
+
+from .api import (
+ TRVApiClient,
+ TRVApiClientCommunicationError,
+ TRVApiClientError,
+)
+from .const import DOMAIN, CALENDAR_NAME, LOCATION_NAME, LOCATION_ID, LOGGER
+
+
+class TRVConfigFLow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle config flow for Wasteplan TRV."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def __init__(self) -> None:
+ """Initialize the config flow."""
+ self._errors: dict[str, str] = {}
+ self._locations: list | None = None
+ self._location_id: str | None = None
+
+ async def async_step_user(
+ self,
+ user_input: dict | None = None,
+ ) -> FlowResult:
+ """Handle a flow initialized by the user."""
+ self._errors = {}
+ self._locations = None
+
+ if user_input is not None:
+ address = user_input[LOCATION_NAME]
+ self._locations = await self._id_from_address(
+ address=address,
+ )
+
+ if self._locations is not None:
+ return await self.async_step_location()
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema({vol.Required(LOCATION_NAME): str}),
+ errors=self._errors,
+ )
+
+ async def async_step_location(
+ self,
+ user_input: dict | None = None,
+ ) -> FlowResult:
+ """Handle location select"""
+ self._errors = {}
+
+ assert self._locations is not None
+
+ if user_input is not None:
+ address = user_input[LOCATION_NAME]
+ locations = [
+ location
+ for location in self._locations
+ if location["adresse"] == address
+ ]
+ location = locations[0]
+ location_id = location["id"]
+ calendar_name = user_input.get(CALENDAR_NAME)
+
+ return self.async_create_entry(
+ title=address,
+ data={
+ CALENDAR_NAME: calendar_name,
+ LOCATION_ID: location_id,
+ },
+ )
+
+ return self.async_show_form(
+ step_id="location",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CALENDAR_NAME): str,
+ vol.Required(LOCATION_NAME): vol.In(
+ [address["adresse"] for address in self._locations]
+ ),
+ }
+ ),
+ errors=self._errors,
+ )
+
+ async def _id_from_address(self, address: str) -> None:
+ """Validate location."""
+ client = TRVApiClient(
+ address=address,
+ location_id="",
+ session=async_create_clientsession(self.hass),
+ )
+
+ try:
+ locations = await client.async_lookup_address()
+ if len(locations) == 0:
+ self._errors["base"] = "no_location"
+ return None
+ return locations
+ except TRVApiClientCommunicationError as exception:
+ LOGGER.error(exception)
+ self._errors["base"] = "connection"
+ except TRVApiClientError as exception:
+ LOGGER.exception(exception)
+ self._errors["base"] = "unknown"
diff --git a/custom_components/wasteplan_trv/const.py b/custom_components/wasteplan_trv/const.py
index 9cfc401..328c0ee 100644
--- a/custom_components/wasteplan_trv/const.py
+++ b/custom_components/wasteplan_trv/const.py
@@ -1,57 +1,12 @@
-ATTRIBUTION = "Data provided by https://trv.no"
+"""Constants for Wasteplan TRV integration."""
+from logging import Logger, getLogger
-CONF_PICKUP_ID = "id"
-CONF_PICKUP_DAY = "pickup_day"
+LOGGER: Logger = getLogger(__package__)
-URL = "https://trv.no/wp-json/wasteplan/v1/calendar/"
+NAME = "Wasteplan TRV"
+DOMAIN = "wasteplan_trv"
+ATTRIBUTION = "Data provided by Trondheim Renholdsverk"
-WASTE_TYPES = {
- "Restavfall": [
- "mdi:recycle", # Default
- "mdi:delete-alert", # Today
- "mdi:delete-alert-outline", # Tomorrow
- "mdi:delete-clock-outline", # This week
- "mdi:delete-empty-outline", # Emptied
- "mdi:delete-restore", # Next week
- ],
- "Papir": [
- "mdi:file", # Default
- "mdi:delete-alert", # Today
- "mdi:delete-alert-outline", # Tomorrow
- "mdi:delete-clock-outline", # This week
- "mdi:delete-empty-outline", # Emptied
- "mdi:delete-restore", # Next week
- ],
- "Plastemballasje": [
- "mdi:bottle-soda", # Default
- "mdi:delete-alert", # Today
- "mdi:delete-alert-outline", # Tomorrow
- "mdi:delete-clock-outline", # This week
- "mdi:delete-empty-outline", # Emptied
- "mdi:delete-restore", # Next week
- ],
- "Hageavfall": [
- "mdi:apple", # Default
- "mdi:delete-alert", # Today
- "mdi:delete-alert-outline", # Tomorrow
- "mdi:delete-clock-outline", # This week
- "mdi:delete-empty-outline", # Emptied
- "mdi:delete-restore", # Next week
- ],
- "Tømmefri uke": [
- "mdi:delete-forever-outline", # Default
- "mdi:delete-forever-outline", # Today
- "mdi:delete-forever-outline", # Tomorrow
- "mdi:delete-forever-outline", # This week
- "mdi:delete-forever-outline", # Emptied
- "mdi:delete-forever-outline", # Next week
- ],
- "Farlig avfall": [
- "mdi:skull-crossbones", # Default
- "mdi:skull-scan", # Today
- "mdi:skull-scan-outline", # Tomorrow
- "mdi:skull-scan-outline", # This week
- "mdi:skull-outline", # Emptied
- "mdi:skull-crossbones-outline", # Next week
- ],
-}
+CALENDAR_NAME = "calendar_name"
+LOCATION_NAME = "address"
+LOCATION_ID = "location"
diff --git a/custom_components/wasteplan_trv/icons/farligavfall.png b/custom_components/wasteplan_trv/icons/farligavfall.png
deleted file mode 100644
index 11362cd..0000000
Binary files a/custom_components/wasteplan_trv/icons/farligavfall.png and /dev/null differ
diff --git a/custom_components/wasteplan_trv/icons/farligavfall.svg b/custom_components/wasteplan_trv/icons/farligavfall.svg
deleted file mode 100644
index 8c39566..0000000
--- a/custom_components/wasteplan_trv/icons/farligavfall.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
\ No newline at end of file
diff --git a/custom_components/wasteplan_trv/icons/glassogmetall.png b/custom_components/wasteplan_trv/icons/glassogmetall.png
deleted file mode 100644
index 98d0526..0000000
Binary files a/custom_components/wasteplan_trv/icons/glassogmetall.png and /dev/null differ
diff --git a/custom_components/wasteplan_trv/icons/glassogmetall.svg b/custom_components/wasteplan_trv/icons/glassogmetall.svg
deleted file mode 100644
index 1c23114..0000000
--- a/custom_components/wasteplan_trv/icons/glassogmetall.svg
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
\ No newline at end of file
diff --git a/custom_components/wasteplan_trv/icons/pappogpapir.png b/custom_components/wasteplan_trv/icons/pappogpapir.png
deleted file mode 100644
index a472ca7..0000000
Binary files a/custom_components/wasteplan_trv/icons/pappogpapir.png and /dev/null differ
diff --git a/custom_components/wasteplan_trv/icons/pappogpapir.svg b/custom_components/wasteplan_trv/icons/pappogpapir.svg
deleted file mode 100644
index d0a227d..0000000
--- a/custom_components/wasteplan_trv/icons/pappogpapir.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
\ No newline at end of file
diff --git a/custom_components/wasteplan_trv/icons/plastemballasje.png b/custom_components/wasteplan_trv/icons/plastemballasje.png
deleted file mode 100644
index c183273..0000000
Binary files a/custom_components/wasteplan_trv/icons/plastemballasje.png and /dev/null differ
diff --git a/custom_components/wasteplan_trv/icons/plastemballasje.svg b/custom_components/wasteplan_trv/icons/plastemballasje.svg
deleted file mode 100644
index 708fbe4..0000000
--- a/custom_components/wasteplan_trv/icons/plastemballasje.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
\ No newline at end of file
diff --git a/custom_components/wasteplan_trv/icons/restavfall.png b/custom_components/wasteplan_trv/icons/restavfall.png
deleted file mode 100644
index 0334f2f..0000000
Binary files a/custom_components/wasteplan_trv/icons/restavfall.png and /dev/null differ
diff --git a/custom_components/wasteplan_trv/icons/restavfall.svg b/custom_components/wasteplan_trv/icons/restavfall.svg
deleted file mode 100644
index 776dbea..0000000
--- a/custom_components/wasteplan_trv/icons/restavfall.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
\ No newline at end of file
diff --git a/custom_components/wasteplan_trv/manifest.json b/custom_components/wasteplan_trv/manifest.json
index e1c84bd..7385bbd 100644
--- a/custom_components/wasteplan_trv/manifest.json
+++ b/custom_components/wasteplan_trv/manifest.json
@@ -1,13 +1,12 @@
{
"domain": "wasteplan_trv",
"name": "Wasteplan TRV",
- "documentation": "https://github.com/jonkristian/wasteplan_trv",
- "issue_tracker": "https://github.com/jonkristian/wasteplan_trv/issues",
- "dependencies": [],
"codeowners": [
"@jonkristian"
],
- "requirements": [],
+ "config_flow": true,
+ "documentation": "https://github.com/jonkristian/wasteplan_trv",
"iot_class": "cloud_polling",
- "version": "1.0.1"
+ "issue_tracker": "https://github.com/jonkristian/wasteplan_trv/issues",
+ "version": "2.0.0"
}
\ No newline at end of file
diff --git a/custom_components/wasteplan_trv/sensor.py b/custom_components/wasteplan_trv/sensor.py
deleted file mode 100644
index 0ba1486..0000000
--- a/custom_components/wasteplan_trv/sensor.py
+++ /dev/null
@@ -1,186 +0,0 @@
-"""Platform for sensor integration."""
-from datetime import datetime as date, timedelta
-import logging
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import ATTR_ATTRIBUTION
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
-import homeassistant.util.dt as dt_util
-import requests
-import voluptuous as vol
-
-from .const import (
- ATTRIBUTION,
- CONF_PICKUP_ID,
- CONF_PICKUP_DAY,
- WASTE_TYPES,
- URL,
-)
-
-_LOGGER = logging.getLogger(__name__)
-
-SCAN_INTERVAL = timedelta(hours=5)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {vol.Required(CONF_PICKUP_ID): cv.string, vol.Optional(CONF_PICKUP_DAY): cv.string}
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the TRV sensor."""
- id = config.get(CONF_PICKUP_ID)
-
- pickup_day = 0
- if config.get(CONF_PICKUP_DAY):
- pickup_day = int(config.get(CONF_PICKUP_DAY))
- data = TRVData(id)
- data.update()
-
- sensors = []
- for wastetype in WASTE_TYPES:
- sensors.append(TRVSensor(wastetype, data, pickup_day))
-
- add_entities(sensors, True)
-
-
-class TRVData:
- """Get the latest data for all authorities."""
-
- def __init__(self, id):
- """Initialize the object."""
- self.data = None
- self.id = id
-
- # Update only once in scan interval.
- @Throttle(SCAN_INTERVAL)
- def update(self):
- """Get the latest data from TRV."""
- response = requests.get(URL + self.id, timeout=10)
- if response.status_code != 200:
- _LOGGER.warning("Invalid response from TRV API")
- else:
- self.data = response.json()
-
-
-class TRVSensor(Entity):
- """Single authority wasteplan sensor."""
-
- def __init__(self, name, data, pickup_day):
- """Initialize the sensor."""
- self._data = data
- self._name = name
- self._icon = WASTE_TYPES[self._name][0]
- self._state = "Ikke bestemt"
- self._year = None
- self._pickup_this_week = False
- self._next_pickup_week = None
- self._date_week_start = None
- self._date_week_end = None
- self._description = None
- self._pickup_day = pickup_day
- self.attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def icon(self):
- """Return the icon."""
- return self._icon
-
- @property
- def extra_state_attributes(self):
- """Return other details about the sensor state."""
- self.attrs["next_pickup_week"] = self._next_pickup_week
- self.attrs["pickup_this_week"] = self._pickup_this_week
- self.attrs["description"] = self._description
- self.attrs["date_week_start"] = self._date_week_start
- self.attrs["date_week_end"] = self._date_week_end
-
- return self.attrs
-
- def update(self):
- """Update the sensor."""
- self._data.update()
-
- containers = []
- today = dt_util.now().weekday()
- this_week = dt_util.now().isocalendar()[1]
-
- # Iterate and stop on match.
- for entry in self._data.data["calendar"]:
-
- # Containers? Make a list.
- if "," in entry["wastetype"]:
- containers = map(str.strip, entry["wastetype"].split(","))
-
- # Do we have a match?
- if self._name == entry["wastetype"] or self._name in containers:
- self._year = entry["year"]
- self._next_pickup_week = entry["week"]
- self._date_week_start = entry["date_week_start"]
- self._date_week_end = entry["date_week_end"]
-
- self._state, self._icon = self.pickup_state()
-
- if entry["week"] == this_week:
- self._pickup_this_week = True
- if today <= self._pickup_day:
- break
-
- elif entry["week"] > this_week:
- break
-
- def pickup_state(self):
- state = None
- icon = None
-
- year = dt_util.now().year
- today = dt_util.now().weekday()
- tomorrow = (dt_util.now() + timedelta(1)).weekday()
- weeks_until = 0
- this_week = dt_util.now().isocalendar()[1]
-
- weeks_until = self._next_pickup_week - this_week
-
- if 0 == weeks_until:
-
- if self._name.startswith("Tømmefri"):
- state = "Denne uken"
- icon = WASTE_TYPES[self._name][3]
- elif today == self._pickup_day:
- state = "I dag"
- icon = WASTE_TYPES[self._name][1]
- elif tomorrow == self._pickup_day:
- state = "I morgen"
- icon = WASTE_TYPES[self._name][2]
- elif today < self._pickup_day or self._pickup_this_week:
- state = "Denne uken"
- icon = WASTE_TYPES[self._name][3]
- else:
- state = "Tømt"
- icon = WASTE_TYPES[self._name][4]
-
- elif 1 == weeks_until:
-
- state = "Neste uke"
- icon = WASTE_TYPES[self._name][4]
-
- elif year < self._year:
- state = "Uke " + str(self._next_pickup_week) + " (" + str(self._year) + ")"
- icon = WASTE_TYPES[self._name][5]
-
- else:
- state = "Uke " + str(self._next_pickup_week)
- icon = WASTE_TYPES[self._name][5]
-
- return state, icon
diff --git a/custom_components/wasteplan_trv/strings.json b/custom_components/wasteplan_trv/strings.json
new file mode 100644
index 0000000..eb484e9
--- /dev/null
+++ b/custom_components/wasteplan_trv/strings.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "location_name": "Address"
+ },
+ "description": "Fill out your address"
+ },
+ "location": {
+ "data": {
+ "calendar_name": "Calendar name",
+ "location_id": "Location ID"
+ },
+ "description": "Pick your location"
+ }
+ },
+ "error": {
+ "no_location": "Unable to find location based on your address",
+ "connection": "Unable to connect to the server.",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/custom_components/wasteplan_trv/translations/en.json b/custom_components/wasteplan_trv/translations/en.json
new file mode 100644
index 0000000..eb484e9
--- /dev/null
+++ b/custom_components/wasteplan_trv/translations/en.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "location_name": "Address"
+ },
+ "description": "Fill out your address"
+ },
+ "location": {
+ "data": {
+ "calendar_name": "Calendar name",
+ "location_id": "Location ID"
+ },
+ "description": "Pick your location"
+ }
+ },
+ "error": {
+ "no_location": "Unable to find location based on your address",
+ "connection": "Unable to connect to the server.",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/custom_components/wasteplan_trv/translations/nb.json b/custom_components/wasteplan_trv/translations/nb.json
new file mode 100644
index 0000000..4ec85b0
--- /dev/null
+++ b/custom_components/wasteplan_trv/translations/nb.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "location_name": "Adresse"
+ },
+ "description": "Skriv inn din adresse"
+ },
+ "location": {
+ "data": {
+ "calendar_name": "Kalendernavn",
+ "location_id": "Lokasjons ID"
+ },
+ "description": "Velg din lokasjon"
+ }
+ },
+ "error": {
+ "no_location": "Fant ingen lokasjoner fra denne adressen.",
+ "connection": "Kunne ikke kontakte til server.",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/example.png b/example.png
new file mode 100644
index 0000000..c22e3de
Binary files /dev/null and b/example.png differ
diff --git a/hacs.json b/hacs.json
index dd7f144..df73baf 100644
--- a/hacs.json
+++ b/hacs.json
@@ -1,6 +1,6 @@
{
"name": "Wasteplan TRV",
"country": "NO",
- "domains": "sensor",
+ "domains": "calendar",
"render_readme": true
}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..17dfa6b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+pip>=21.0,<23.1
+colorlog
+homeassistant
diff --git a/scripts/develop b/scripts/develop
new file mode 100755
index 0000000..49de63b
--- /dev/null
+++ b/scripts/develop
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+# Start Home Assistant
+hass -c . --debug
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/setup.cfg b/setup.cfg
index 17c1143..dc6564a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,7 +24,7 @@ include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88
-indent = " "
+indent = " "
# by default isort don't check module indexes
not_skip = __init__.py
# will group `import x` and `from x import` of the same module.