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 ❤️ -Buy Me A Coffee +[![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 @@ - - - - Slice 1 - Created with Sketch. - - - - - - - - - \ 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 @@ - - - - Slice 1 - Created with Sketch. - - - - - - - - - - - - - - - - \ 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 @@ - - - - papir - Created with Sketch. - - - - - - - - - - - - - - - - - - \ 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 @@ - - - - Slice 1 - Created with Sketch. - - - - - - - - - \ 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 @@ - - - - Slice 1 - Created with Sketch. - - - - - - - - \ 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.