From 162483ba3cbc5eaed45e2dcbfa0a245b4124f36a Mon Sep 17 00:00:00 2001 From: Morgane Date: Wed, 17 Jun 2026 11:16:24 +0200 Subject: [PATCH] =?UTF-8?q?Fork=20garbage=5Fcollection=20avec=20d=C3=A9cal?= =?UTF-8?q?age=20jours=20f=C3=A9ri=C3=A9s=20FR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 + README.md | 514 +++++++++++ custom_components/__init__.py | 1 + .../garbage_collection/__init__.py | 372 ++++++++ .../garbage_collection/calendar.py | 151 ++++ .../garbage_collection/config_flow.py | 281 ++++++ custom_components/garbage_collection/const.py | 144 +++ .../garbage_collection/diagnostics.py | 29 + .../garbage_collection/helpers.py | 67 ++ .../garbage_collection/manifest.json | 18 + .../garbage_collection/sensor.py | 840 ++++++++++++++++++ .../garbage_collection/services.yaml | 60 ++ .../garbage_collection/translations/cs.json | 104 +++ .../garbage_collection/translations/da.json | 105 +++ .../garbage_collection/translations/de.json | 104 +++ .../garbage_collection/translations/en.json | 108 +++ .../garbage_collection/translations/es.json | 105 +++ .../garbage_collection/translations/et.json | 105 +++ .../garbage_collection/translations/fr.json | 110 +++ .../garbage_collection/translations/it.json | 105 +++ .../garbage_collection/translations/pl.json | 105 +++ .../translations/pt-BR.json | 105 +++ .../translations/sensor.cs.json | 8 + .../translations/sensor.da.json | 8 + .../translations/sensor.de.json | 8 + .../translations/sensor.en.json | 8 + .../translations/sensor.es.json | 8 + .../translations/sensor.et.json | 8 + .../translations/sensor.fr.json | 8 + .../translations/sensor.it.json | 8 + .../translations/sensor.nl.json | 8 + .../translations/sensor.no.json | 8 + .../translations/sensor.pl.json | 8 + .../translations/sensor.pt-BR.json | 8 + .../translations/sensor.se.json | 8 + .../translations/sensor.sk.json | 8 + .../translations/sensor.sl.json | 8 + .../garbage_collection/translations/sk.json | 179 ++++ .../garbage_collection/translations/sl.json | 179 ++++ hacs.json | 5 + info.md | 76 ++ requirements.txt | 2 + requirements_tests.txt | 5 + 43 files changed, 4120 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 custom_components/__init__.py create mode 100644 custom_components/garbage_collection/__init__.py create mode 100644 custom_components/garbage_collection/calendar.py create mode 100644 custom_components/garbage_collection/config_flow.py create mode 100644 custom_components/garbage_collection/const.py create mode 100644 custom_components/garbage_collection/diagnostics.py create mode 100644 custom_components/garbage_collection/helpers.py create mode 100644 custom_components/garbage_collection/manifest.json create mode 100644 custom_components/garbage_collection/sensor.py create mode 100644 custom_components/garbage_collection/services.yaml create mode 100644 custom_components/garbage_collection/translations/cs.json create mode 100644 custom_components/garbage_collection/translations/da.json create mode 100644 custom_components/garbage_collection/translations/de.json create mode 100644 custom_components/garbage_collection/translations/en.json create mode 100644 custom_components/garbage_collection/translations/es.json create mode 100644 custom_components/garbage_collection/translations/et.json create mode 100644 custom_components/garbage_collection/translations/fr.json create mode 100644 custom_components/garbage_collection/translations/it.json create mode 100644 custom_components/garbage_collection/translations/pl.json create mode 100644 custom_components/garbage_collection/translations/pt-BR.json create mode 100644 custom_components/garbage_collection/translations/sensor.cs.json create mode 100644 custom_components/garbage_collection/translations/sensor.da.json create mode 100644 custom_components/garbage_collection/translations/sensor.de.json create mode 100644 custom_components/garbage_collection/translations/sensor.en.json create mode 100644 custom_components/garbage_collection/translations/sensor.es.json create mode 100644 custom_components/garbage_collection/translations/sensor.et.json create mode 100644 custom_components/garbage_collection/translations/sensor.fr.json create mode 100644 custom_components/garbage_collection/translations/sensor.it.json create mode 100644 custom_components/garbage_collection/translations/sensor.nl.json create mode 100644 custom_components/garbage_collection/translations/sensor.no.json create mode 100644 custom_components/garbage_collection/translations/sensor.pl.json create mode 100644 custom_components/garbage_collection/translations/sensor.pt-BR.json create mode 100644 custom_components/garbage_collection/translations/sensor.se.json create mode 100644 custom_components/garbage_collection/translations/sensor.sk.json create mode 100644 custom_components/garbage_collection/translations/sensor.sl.json create mode 100644 custom_components/garbage_collection/translations/sk.json create mode 100644 custom_components/garbage_collection/translations/sl.json create mode 100644 hacs.json create mode 100644 info.md create mode 100644 requirements.txt create mode 100644 requirements_tests.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..45eb252 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 bruxy70 + +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 new file mode 100644 index 0000000..6017ccb --- /dev/null +++ b/README.md @@ -0,0 +1,514 @@ +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) [![Garbage-Collection](https://img.shields.io/github/v/release/bruxy70/Garbage-Collection.svg?1)](https://github.com/bruxy70/Garbage-Collection) ![Maintenance](https://img.shields.io/maintenance/yes/2022.svg) + +## Fork notes + +This is a personal fork of [bruxy70/Garbage-Collection](https://github.com/bruxy70/Garbage-Collection) (original project archived/unmaintained since end of 2022). + +Added on top of the original: a built-in **"Décaler la collecte si jour férié dans la semaine"** option (config + options flow, both steps), available for every frequency. When enabled, it uses the [`holidays`](https://pypi.org/project/holidays/) Python library (no separate calendar entity needed) for the configured country code (`holiday_country`, default `FR`). For each calculated collection date, if a public holiday falls on that day or any earlier day in the same ISO week, the collection date is pushed forward by one day, cascading if the new day also lands on a holiday. This reproduces the logic of the original project's `holiday_in_week` blueprint, but built directly into the integration so no separate blueprint or external "Holidays" helper is required. + +## End of Support (upstream) + +Home Assistant has introduced local calendars in 2022, in the 2023.1 release they added an option for different recurent events. With this, most of the functionality of this custom helper is supported natively. So I will end developing and supporting this helper in 2023. + +## Table of Contents + +- [Description](#garbage-collection) +- [Installation](#installation) + + - [Manual Installation](#manual-installation) + - [Installation via Home Assistant Community Store (HACS)](#installation-via-home-assistant-community-store-hacs) + +- [Configuration](#configuration) +- [Blueprints for Manual Update](#blueprints-for-manual-update) + - [Public Holidays](#public-holidays) + - [Include and Exclude](#include-and-exclude) + - [Offset](#offset) + - [Import TXT](#import-txt) + - [Monthly on a fixed date](#monthly-on-a-fixed-date) +- [State and Attributes](#state-and-attributes) +- [Lovelace configuration examples](#lovelace-config-examples) + +# Garbage Collection + +The `garbage_collection` component is a Home Assistant helper that creates a custom sensor for monitoring a regular garbage collection schedule. The sensor can be configured for a number of different patterns: + +- `weekly` schedule (including multiple collection days, e.g. on Tuesday and Thursday) +- `every-n-weeks` repeats every `period` of weeks, starting from the week number `first_week`. It uses the week number - therefore, it restarts each year, as the weeks start from number 1 each year. +- bi-weekly in `even-weeks` or `odd-weeks` (technically, it is the same as every 2 weeks with 1st or 2nd `first_week`) +- `every-n-days` (repeats regularly from the given first date). If n is a multiply of 7, it works similar to `every-n-weeks`, with the difference that it does not use the week numbers (that restart each year) but continues infinitely from the initial date. +- `monthly` schedule (nth weekday each month), or a specific weekday of each nth week. Using the `period` it could also be every 2nd, 3rd etc month. +- `annually` (e.g. birthdays). This is once per year. Using include dates, you can add additional dates manually. +- `blank` does not automatically schedule any collections - to be used in cases where you want to make completely own schedule with `manual_update`. + +You can also configure seasonal calendars (e.g. for bio-waste collection), by configuring the first and last month. +And you can `group` entities, which will merge multiple schedules into one sensor. + +These are some examples using this sensor. The Lovelace config examples are included below. + + + + + + + + +## Installation + +### MANUAL INSTALLATION + +1. Download the + [latest release](https://github.com/bruxy70/garbage_collection/releases/latest). +2. Unpack the release and copy the `custom_components/garbage_collection` directory + into the `custom_components` directory of your Home Assistant + installation. +3. Restart Home Assistant. +4. Configure the `garbage_collection` helper. + +### INSTALLATION VIA Home Assistant Community Store (HACS) + +1. Ensure that [HACS](https://hacs.xyz/) is installed. +2. Search for and install the "Garbage Collection" integration. +3. Restart Home Assistant. +4. Configure the `garbage_collection` helper. + +## Configuration + +Go to `Settings`/`Devices & Services`/`Helpers`, click on the `+ CREATE HELPER` button, select `Garbage Collection` and configure the helper.
If you would like to add more than one collection schedule, click on the `+ CREATE HELPER` button again and add another `Garbage Collection` helper instance. + +**The configuration hapend in 2 steps.** In the first step, you select the `frequency` and common parameters. In the second step you configure additional parameters depending on the selected frequency. + +_The configuration via `configuration.yaml` has been deprecated. If you have previously configured the integration there, it will be imported to ConfigFlow, and you should remove it._ + +### STEP 1 - Common Parameters + +| Parameter | Required | Description | +| :------------------ | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Friendly name` | Yes | Sensor friendly name | +| `Frequency` | Yes | `"weekly"`, `"even-weeks"`, `"odd-weeks"`, `"every-n-weeks"`, `"every-n-days"`, `"monthly"`, `"annual"`, `"group"` or `"blank"` | +| `Icon` | No | Default icon **Default**: `mdi:trash-can` | +| `Icon today` | No | Icon if the collection is today **Default**: `mdi:delete-restore` | +| `Icon tomorrow` | No | Icon if the collection is tomorrow **Default**: `mdi:delete-circle` | +| `Expire After` | No | Time in format format `HH:MM`. If the collection is due today, start looking for the next occurence after this time (i.e. if the weekly collection is in the morning, change the state from 'today' to next week in the afternoon) | +| `Verbose state` | No | The sensor state will show collection date and remaining days, instead of number.
**Obsolete** - do the formatting on the dashboard instead.
**Default**: `False` | +| `Hidde in calendar` | No | Hide in calendar (useful for sensors that are used in groups)
**Default**: `False` | +| `Manual update` | No | (Advanced). Do not automatically update the status. Status is updated manualy by calling the service `garbage_collection.update_state` from an automation triggered by event `garbage_collection_loaded`, that could manually add or remove collection dates, and manually trigger the state update at the end. [See the example](#manual-update-examples).
**Default**: `False` | +| `Verbose format` | No | (relevant when `verbose state` is `True`). Verbose status formatting string. Can use placeholders `{date}` and `{days}` to show the date of next collection and remaining days.
**Obsolete** - do the formatting on the dashboard instead.
**Default**: `'on {date}, in {days} days'`
_When the collection is today or tomorrow, it will show `Today` or `Tomorrow`_
_(currently in English, French, Czech and Italian)._ | +| `Date format` | No | In the `verbose format`, you can configure the format of date (using [strftime](http://strftime.org/) format)
**Obsolete** - do the formatting on the dashboard instead.
**Default**: `'%d-%b-%Y'` | + +### STEP 2 - parameters depending on the selected frequency + +#### ...FOR ALL FREQUENCIES EXCEPT ANNUAL, GROUP and BLANK + +| Parameter | Required | Description | +| :------------ | :------- | :--------------------------------------------------------------------------------- | +| `First month` | No | Month three letter abbreviation, e.g. `"jan"`, `"feb"`...
**Default**: `"jan"` | +| `Last month` | No | Month three letter abbreviation.
**Default**: `"dec"` | + +#### ...FOR ALL FREQUENCIES EXCEPT ANNUAL, EVERY-N-DAYS, GROUP and BLANK + +| Parameter | Required | Description | +| :---------------- | :------- | :---------------------------------------------------------------------------------------------------- | +| `Collection days` | Yes | Day three letter abbreviation, list of `"mon"`, `"tue"`, `"wed"`, `"thu"`, `"fri"`, `"sat"`, `"sun"`. | + +#### ...FOR COLLECTION EVERY-N-WEEKS + +| Parameter | Required | Description | +| :----------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Period` | No | Collection every `"period"` weeks (integer 1-53)
**Default**: 1 | +| `First week` | No | First collection on the `"first week"` week (integer 1-53)
**Default**: 1
_(The week number is using [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601#Week_dates) numeric representation of the week)

Note: This parameter cannot be used to set the beginning of the collection period (use the `first month` parameter for that). The purpose of `first week` is to simply 'offset' the week number, so the collection every ;'n' weeks does not always trigger on week numbers that are multiplication of 'n'. Technically, the value of this parameter shall be less than `period`, otherwise it will give weird results. Also note, that the week numbers restart each year. Use `every-n-days` frequency if you need a consistent period across the year ends._ | + +#### ...FOR COLLECTION EVERY-N-DAYS + +| Parameter | Required | Description | +| :----------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `First date` | Yes | Repeats every n days from this first date
(date in the international ISO format `'yyyy-mm-dd'`). | +| `Period` | No | Collection every `"period"` days (warning - in this configuration, it is days, not weeks!)
**Default**: 1 (daily, which makes no sense I suppose) | + +#### ...FOR MONTHLY COLLECTION + +The monthly schedule has two flavors: it can trigger either on the **nth occurrence of the weekday** in a month, or on the weekday in the **nth week** of each month. + +| Parameter | Required | Description | +| :---------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Order of weekday` | Yes | List of week numbers of `collection day` each month. E.g., if `collection_day` is `"sat"`, 1 will mean 1st Saturday each month (integer 1-5) | +| `Order of week, instead of weekday order` | No | **CONFIGURE THIS ONE ONLY IF YOU ARE SURE YOU NEED IT**. This will **alter** the behaviour of `order of weekday`, so that instead of nth weekday of each month, take the weekday of the nth week of each month.
So if the month starts on Friday, the Wednesday of the 1st week would actually be last Wednesday of the previous month and the Wednesday of 2nd week will be the 1st Wednesday of the month. So if you have just randomy clicked on the option, it might appear as if it calculates a wrong date! Yes, this is confusing, but there are apparently some use case for this. | +| `Period` | No | If `period` is not defined (or 1), the schedule will repeat monthly. If `period` is 2, it will be every 2nd month. If `period` is 3, it will be once per quarter, and so on.
The `first month` parameter will then define the starting month. So if the `first month` is `jan` (or not defined), and `period` is 2, the collection will be in odd months (`jan`, `mar`, `may`, `jul`, `sep` and `nov`). If `first month` is `feb`, it will be in even months. (integer 1-12)
**Default**: 1 | + +#### ...FOR ANNUAL COLLECTION + +| Parameter | Required | Description | +| :-------- | :------- | :----------------------------------------------------------------------------------- | +| `Date` | Yes | The date of collection, in format `'mm/dd'` (e.g. '11/24' for November 24 each year) | + +#### ...FOR GROUP + +| Parameter | Required | Description | +| :----------------- | :------- | :------------------------------ | +| `List of entities` | Yes | A list of `entity_id`s to merge | + +## Blueprints for Manual Update + +### Prerequisites + +1. To use the **blueprints**, you need to set the `garbage_collection` entity for `Manual update`, that will fire the `garbage_collection_loaded` event on each sensor update and trigger the automation **blueprint**. +2. Install/Import **blueprint** +3. From the **blueprint**, create and configure the automation + +### Public Holidays + +There are a couple of **blueprints**, automatically moving the collection falling on a public holiday. Or if there was a public holiday in the week before the scheduled collection. + +The Public Holidays **blueprints** use a separate custom helper **Holidays**, available through **HACS**, that you can configure for different countries. + +| | | +| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fmove_on_holiday.yaml) | Move the collection to the next day, if the collection falls on public holiday | +| [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fmove_on_holiday_with_include_exclude.yaml) | Remove events falling on provided "exclude" list of dates. Then check the calendar of public holidays and move events that fall on a public holiday to the next day. Finally, add additional events on dates from "include" list. | +| [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fholiday_in_week.yaml) | Move forward one day if a public holiday was in the collection week, before or on the collection day (and keep moving if the new collection day also falls on a holiday) | +| [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fmultiple_holidays_in_week.yaml) | Move forward by a day for each public holiday was in the collection week, before or on the collection day (and keep moving if the new collection day also falls on a holiday). So if there were two public holidays, move by two days. | +| [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fmove_on_holiday_carry_over.yaml) | Move forward by one day if there was a public holiday in the collection week, before or on the collection day (and keep moving if the new collection day also falls on a holiday). Only move by one day, but if there was more than one public holiday in the week, carry it over to the following week. So if there were 2 public holidays this week, move it by one day this week and one day next week. | +| [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fskip_holday.yaml) | Skip the holiday | + +### Include and Exclude + +A list of fixed dates to include and exclude from the calculated schedule. + +| | | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------ | +| [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Finclude_exclude.yaml) | Include and Exclude | +| [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Finclude.yaml) | Include | +| [![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fexclude.yaml) | Exclude | + +### Offset + +The offset blueprint will move the calculated collections by a number of days. This can be used, for example, to schedule collection for last Saturday each month - just set the collection to the first Saturday each month and offset it by -7 days. + +[![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Foffset.yaml) + +### Import txt + +This **blueprint** requires a `command_line` sensor reading content of a txt file, containig a set of dates, one per line. + +[![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fimport_txt.yaml) + +
+command_line sensor example + +The Home Assistant command line sensor has a 255 character limitation. For many people this causes an issue where the sensor only imports part of the list. To overcome this limitation, @stu1811 came up with this brilliant solution. + +Create a script that shows next 20 dates from the current day forward. + +#### **`/share/import_dates.sh`** + +```bash +#!/bin/bash +new_dates=$(for x in $(cat /share/import_dates.txt); do + if [[ $(date -d $x +"%y%m%d") -ge $(date +"%y%m%d") ]]; then + echo $x + fi + done) +echo "$new_dates" | head -n20 +``` + +Then, create the command line sensor taking the output of this script. + +#### **`configuration.yaml`** + +```yaml +sensor: + - platform: command_line + name: Import dates + command: "sh /share/import_dates.sh" +``` + +
+ +### Monthly on a fixed date + +This will create a schedule on a fixed date each month. For example on the 3rd each month. The helper does not allow it, as it is generally designed around paterns evolving around weekly schedules (since garbage collection typically happens on a set day in a week, rather than set day in a month). But few of you wanted that, so here you go. + +[![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fmonthly_fixed_date.yaml) One fixed date + +[![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbruxy70%2FGarbage-Collection%2Fblob%2Fmaster%2Fblueprints%2Fmonthly_fixed_two_dates.yaml) Two dates + +## STATE AND ATTRIBUTES + +### State + +The state can be one of + +| Value | Meaning | +| :---- | :--------------------- | +| `0` | Collection is today | +| `1` | Collection is tomorrow | +| `2` | Collection is later | + +If the `verbose_state` parameter is set, it will show the date, and the remaining days. For example: "Today" or "Tomorrow" or "on 10-Sep-2019, in 2 days" (configurable) + +### Attributes + +| Attribute | Description | +| :---------------- | :--------------------------------------- | +| `next_date` | The date of next collection | +| `days` | Days till the next collection | +| `last_collection` | The date and time of the last collection | + +## Services + +### `garbage_collection.collect_garbage` + +If the collection is scheduled for today, mark it completed and look for the next collection. +It will set the `last_collection` attribute to the current date and time. + +| Attribute | Description | +| :---------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `entity_id` | The garbage collection entity id (e.g. `sensor.general_waste`) | +| `last_collection` | (optional) Set the last collection date to this value. This can be used to re-set the next collection calculation, if the last collection date was set in error. If omitted, it will set the last collection to the current date & time. | + +## Manual update + +There are standard [blueprints](#blueprints-for-manual-update) provided to handle manual updates - to move collection on public holidays or offset the collection. + +If these **blueprints** do not work for you, you can create your own custom rules to handle any scenario. If you do so, please share the blueprints with the others by posting them to the [blueprints directory](https://github.com/bruxy70/Garbage-Collection/tree/master/blueprints) - someone else might find them useful. Thanks! +To help you with creating custom automations, see the following examples: + +## _!!! Advanced !!! If you think this is too complicated, then this is not for you!!!_ + +
+ +## Services used for `manual_update` + +The following services are used within automations, triggered by the [garbage_collection_loaded](#garbage_collection_loaded) event. Don't use them anywhere else, it won't work. For the examples of their use, see the [examples](#manual-update-examples) + +```mermaid +flowchart TD + A[HA updates entity] -->B[triggered garbage_collection_loaded event] + B --> C[calling services] + C --> D[garbage_collection.add_date] & E[garbage_collection.remove_date] & F[garbage_collection.offset_date] --> C + D & E & F --> G[manual update finished -> garbage_collection.update_state] +``` + +### `garbage_collection.add_date` + +Add a date to the list of dates calculated automatically. To add multiple dates, call this service multiple-times with different dates. +Note that this date will be removed on the next sensor update, when the data is re-calculated and loaded. This is why, this service should be called from the automation triggered by the event `garbage_collection_loaded`. This event is called each time the sensor is updated. And at the end of this automation, you need to call the `garbage_collection.update_state` service to update the sensor state based on automatically collected dates, and the dates added, removed, or offset by the automation. + +| Attribute | Description | +| :---------- | :------------------------------------------------------------------------------------------- | +| `entity_id` | The garbage collection entity id (e.g. `sensor.general_waste`) | +| `date` | The date to be added, in ISO format (`'yyyy-mm-dd'`). Make sure to enter the date in quotes! | + +### `garbage_collection.remove_date` + +Remove a date to the list of dates calculated automatically. To remove multiple dates, call this service multiple-times with different dates. +Note that this date will be removed on the next sensor update, when the data is re-calculated and loaded. This is why, this service should be called from the automation triggered by the event `garbage_collection_loaded`. This event is called each time the sensor is updated. And at the end of this automation, you need to call the `garbage_collection.update_state` service to update the sensor state based on automatically collected dates, and the dates added, removed, or offset by the automation. + +| Attribute | Description | +| :---------- | :--------------------------------------------------------------------------------------------- | +| `entity_id` | The garbage collection entity id (e.g. `sensor.general_waste`) | +| `date` | The date to be removed, in ISO format (`'yyyy-mm-dd'`). Make sure to enter the date in quotes! | + +### `garbage_collection.offset_date` + +Offset the calculated collection day by the `offset` number of days. +Note that this date will be removed on the next sensor update, when the data is re-calculated and loaded. This is why, this service should be called from the automation triggered by the event `garbage_collection_loaded`. This event is called each time the sensor is updated. And at the end of this automation, you need to call the `garbage_collection.update_state` service to update the sensor state based on automatically collected dates, and the dates added, removed, or offset by the automation. + +| Attribute | Description | +| :---------- | :--------------------------------------------------------------------------------------------- | +| `entity_id` | The garbage collection entity id (e.g. `sensor.general_waste`) | +| `date` | The date to be removed, in ISO format (`'yyyy-mm-dd'`). Make sure to enter the date in quotes! | +| `offset` | By how many days to offset - integer between `-31` to `31` (e.g. `1`) | + +### `garbage_collection.update_state` + +Choose the next collection date from the list of dates calculated automatically, added by service calls (and not removed), and update the entity state and attributes. + +| Attribute | Description | +| :---------- | :------------------------------------------------------------- | +| `entity_id` | The garbage collection entity id (e.g. `sensor.general_waste`) | + +## Events + +### `garbage_collection_loaded` + +This event is triggered each time a `garbage_collection` entity is being updated. You can create an automation to modify the collection schedule before the entity state update. + +Event data: +| Attribute | Description | +| :-- | :-- | +| `entity_id` | The garbage collection entity id (e.g. `sensor.general_waste`) | +| `collection_dates` | List of collection dates calculated automatically. | + +## Manual update examples + +For the example below, the entity should be configured with `manual_update` set to `true`. +Then, when the `garbage_collection` entity is updated (normally once a day at midnight, or restart, or when triggering entity update by script), it will calculate the collection schedule for previous, current and next year. But it will **NOT UPDATE** the entity state. +Instead, it will trigger an event `garbage_collection_loaded` with a list of automatically calculated dates as a parameter. +You will **have to create an automation triggered by this event**. In this automation, you will need to call the service `garbage_collection.update_state` to update the state. Before that, you can call the services `garbage_collection.add_date` and/or `garbage_collection.remove_date` and/or `garbage_collection.offset_date` to programmatically tweak the dates in whatever way you need (e.g. based on values from external API sensor, comparing the dates with the list of holidays, calculating custom offsets based on the day of the week etc.). This is complicated but gives you the ultimate flexibility. + +## Simple example + +Adding an extra collection date (a fixed date in this case) - for the entity `sensor.test`. + +```yaml +alias: garbage_collection event +description: "Manually add a collection date, then trigger entity state update." +trigger: + - platform: event + event_type: garbage_collection_loaded + event_data: + entity_id: sensor.test +action: + - service: garbage_collection.add_date + data: + entity_id: "{{ trigger.event.data.entity_id }}" + date: "2022-01-07" + - service: garbage_collection.update_state + data: + entity_id: sensor.test +mode: single +``` + +## Moderate example + +This will loop through the calculated dates, and add an extra collection to a day after each calculated one. So if this is set for a collection every first Wednesday each month, it will result in a collection on the first Wednesday, and the following day (kind of first Thursday, except if the week is starting on Thursday - just a random weird example :). + +This example is for an entity `sensor.test`. If you want to use it for yours, replace it with the real entity name in the trigger. + +```yaml +alias: test garbage_collection event +description: "Loop through all calculated dates, add extra collection a day after the calculate one" +trigger: + - platform: event + event_type: garbage_collection_loaded + event_data: + entity_id: sensor.test +action: + - repeat: + for_each: "{{ trigger.event.data.collection_dates }}" + sequence: + - service: garbage_collection.add_date + data: + entity_id: "{{ trigger.event.data.entity_id }}" + date: >- + {{( as_datetime(repeat.item) + timedelta( days = 1)) | as_timestamp | timestamp_custom("%Y-%m-%d") }} + - service: garbage_collection.update_state + data: + entity_id: "{{ trigger.event.data.entity_id }}" +mode: single +``` + +## Advanced example + +This is an equivalent of "holiday in week" move - checking if there is a public holiday on the calculated collection day, or earlier in the week. And if yes, moving the collection by one day. This is fully custom logic, so it could be further complicated by whatever rules anyone wants. + +This example is for an entity `sensor.test`. If you want to use it for yours, replace it with a real entity name in the trigger. + +```yaml +alias: test garbage_collection event +description: >- + Loop through all calculated dates, move the collection by 1 day if a public holiday was in the week before or on the calculated collection date calculate one +trigger: + - platform: event + event_type: garbage_collection_loaded + event_data: + entity_id: sensor.test +action: + - repeat: + for_each: "{{ trigger.event.data.collection_dates }}" + sequence: + - condition: template + value_template: >- + {%- set collection_date = as_datetime(repeat.item) %} + {%- set ns = namespace(found=false) %} + {%- for i in range(collection_date.weekday()+1) %} + {%- set d = ( collection_date + timedelta( days=-i) ) | as_timestamp | timestamp_custom("%Y-%m-%d") %} + {%- if d in state_attr(trigger.event.data.entity_id,'holidays') %} + {%- set ns.found = true %} + {%- endif %} + {%- endfor %} + {{ ns.found }} + - service: garbage_collection.offset_date + data: + entity_id: "{{ trigger.event.data.entity_id }}" + date: "{{ repeat.item }}" + offset: 1 + - service: garbage_collection.update_state + data: + entity_id: "{{ trigger.event.data.entity_id }}" +mode: single +``` + +Or you can use the [blueprints](#blueprints-for-manual-update) I made for you. And you are welcome to create your own and share with others. + +
+ +# Lovelace config examples + +For information/inspiration - not supported. + +
+ +## Garbage Collection custom card + +You can use the custom [garbage collection card](https://github.com/amaximus/garbage-collection-card) developed by @amaximus. + + + +## With images (picture-entity) + +This is what I use (I like images). I use a horizontal stack of picture-entities, with `card-templater` plugin ([Lovelace Card Templater](https://github.com/gadgetchnnel/lovelace-card-templater)) to show the number of days: + + + +(The `state` is designed to be used as traffic lights. That's why it has 3 values. You obviously cannot use this with `verbose_state`) + +This is the configuration + +```yaml +- type: "custom:card-templater" + card: + type: picture-entity + name_template: >- + {{ state_attr('sensor.bio','days') }} days + show_name: True + show_state: False + entity: sensor.bio + state_image: + "0": "/local/containers/bio_today.png" + "1": "/local/containers/bio_tomorrow.png" + "2": "/local/containers/bio_off.png" + entities: + - sensor.bio +``` + +## List view (entities) + +The simplest visualization is to use entities. In this case, I use `verbose_state` to show `state` as text. + + + +Lovelace configuration + +```yaml +- type: entities + entities: + - sensor.general_waste + - sensor.bio + - sensor.paper + - sensor.plastic +``` + +## Icon view (glance) + + + +Lovelace Configuration + +```yaml +- type: glance + entities: + - sensor.general_waste +``` + +
diff --git a/custom_components/__init__.py b/custom_components/__init__.py new file mode 100644 index 0000000..74b8a51 --- /dev/null +++ b/custom_components/__init__.py @@ -0,0 +1 @@ +"""Needed for pytest.""" diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py new file mode 100644 index 0000000..32423fc --- /dev/null +++ b/custom_components/garbage_collection/__init__.py @@ -0,0 +1,372 @@ +"""Component to integrate with garbage_colection.""" +from __future__ import annotations + +import asyncio +import logging +from datetime import timedelta +from types import MappingProxyType +from typing import Any, Dict + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +import voluptuous as vol +from dateutil.relativedelta import relativedelta +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_HIDDEN, CONF_ENTITIES, CONF_ENTITY_ID, WEEKDAYS +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers.typing import ConfigType + +from . import const, helpers + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +_LOGGER = logging.getLogger(__name__) + +months = [m["value"] for m in const.MONTH_OPTIONS] +frequencies = [f["value"] for f in const.FREQUENCY_OPTIONS] + +SENSOR_SCHEMA = vol.Schema( + { + vol.Required(const.CONF_FREQUENCY): vol.In(frequencies), + vol.Optional(const.CONF_ICON_NORMAL): cv.icon, + vol.Optional(const.CONF_ICON_TODAY): cv.icon, + vol.Optional(const.CONF_ICON_TOMORROW): cv.icon, + vol.Optional(const.CONF_EXPIRE_AFTER): helpers.time_text, + vol.Optional(const.CONF_VERBOSE_STATE): cv.boolean, + vol.Optional(ATTR_HIDDEN): cv.boolean, + vol.Optional(const.CONF_MANUAL): cv.boolean, + vol.Optional(const.CONF_DATE): helpers.month_day_text, + vol.Optional(CONF_ENTITIES): cv.entity_ids, + vol.Optional(const.CONF_COLLECTION_DAYS): vol.All( + cv.ensure_list, [vol.In(WEEKDAYS)] + ), + vol.Optional(const.CONF_FIRST_MONTH): vol.In(months), + vol.Optional(const.CONF_LAST_MONTH): vol.In(months), + vol.Optional(const.CONF_WEEKDAY_ORDER_NUMBER): vol.All( + cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(min=1, max=5))] + ), + vol.Optional(const.CONF_WEEK_ORDER_NUMBER): vol.All( + cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(min=1, max=5))] + ), + vol.Optional(const.CONF_PERIOD): vol.All( + vol.Coerce(int), vol.Range(min=1, max=1000) + ), + vol.Optional(const.CONF_FIRST_WEEK): vol.All( + vol.Coerce(int), vol.Range(min=1, max=52) + ), + vol.Optional(const.CONF_FIRST_DATE): cv.date, + vol.Optional(const.CONF_VERBOSE_FORMAT): cv.string, + vol.Optional(const.CONF_DATE_FORMAT): cv.string, + }, + extra=vol.ALLOW_EXTRA, +) + +CONFIG_SCHEMA = vol.Schema( + { + const.DOMAIN: vol.Schema( + {vol.Optional(const.CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA])} + ) + }, + extra=vol.ALLOW_EXTRA, +) + +COLLECT_NOW_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(const.ATTR_LAST_COLLECTION): cv.datetime, + } +) + +UPDATE_STATE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]), + } +) + +ADD_REMOVE_DATE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Required(const.CONF_DATE): cv.date, + } +) + +OFFSET_DATE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Required(const.CONF_DATE): cv.date, + vol.Required(const.CONF_OFFSET): vol.All( + vol.Coerce(int), vol.Range(min=-31, max=31) + ), + } +) + + +async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool: + """Set up platform - register services, inicialize data structure.""" + + async def handle_add_date(call: ServiceCall) -> None: + """Handle the add_date service call.""" + entity_ids = call.data.get(CONF_ENTITY_ID, []) + collection_date = call.data.get(const.CONF_DATE) + for entity_id in entity_ids: + _LOGGER.debug("called add_date %s from %s", collection_date, entity_id) + try: + entity = hass.data[const.DOMAIN][const.SENSOR_PLATFORM][entity_id] + await entity.add_date(collection_date) + except KeyError as err: + _LOGGER.error( + "Failed adding date %s to %s (%s)", + collection_date, + entity_id, + err, + ) + + async def handle_remove_date(call: ServiceCall) -> None: + """Handle the remove_date service call.""" + entity_ids = call.data.get(CONF_ENTITY_ID, []) + collection_date = call.data.get(const.CONF_DATE) + for entity_id in entity_ids: + _LOGGER.debug("called remove_date %s from %s", collection_date, entity_id) + try: + entity = hass.data[const.DOMAIN][const.SENSOR_PLATFORM][entity_id] + await entity.remove_date(collection_date) + except KeyError as err: + _LOGGER.error( + "Failed removing date %s from %s (%s)", + collection_date, + entity_id, + err, + ) + + async def handle_offset_date(call: ServiceCall) -> None: + """Handle the offset_date service call.""" + entity_ids = call.data.get(CONF_ENTITY_ID, []) + offset = call.data.get(const.CONF_OFFSET) + collection_date = call.data.get(const.CONF_DATE) + for entity_id in entity_ids: + _LOGGER.debug( + "called offset_date %s by %d days for %s", + collection_date, + offset, + entity_id, + ) + try: + new_date = collection_date + relativedelta( + days=offset + ) # pyright: reportOptionalOperand=false + entity = hass.data[const.DOMAIN][const.SENSOR_PLATFORM][entity_id] + await asyncio.gather( + entity.remove_date(collection_date), entity.add_date(new_date) + ) + except (TypeError, KeyError) as err: + _LOGGER.error("Failed ofsetting date for %s - %s", entity_id, err) + break + + async def handle_update_state(call: ServiceCall) -> None: + """Handle the update_state service call.""" + entity_ids = call.data.get(CONF_ENTITY_ID, []) + for entity_id in entity_ids: + _LOGGER.debug("called update_state for %s", entity_id) + try: + entity = hass.data[const.DOMAIN][const.SENSOR_PLATFORM][entity_id] + entity.update_state() + except KeyError as err: + _LOGGER.error("Failed updating state for %s - %s", entity_id, err) + + async def handle_collect_garbage(call: ServiceCall) -> None: + """Handle the collect_garbage service call.""" + entity_ids = call.data.get(CONF_ENTITY_ID, []) + last_collection = call.data.get(const.ATTR_LAST_COLLECTION, helpers.now()) + for entity_id in entity_ids: + _LOGGER.debug("called collect_garbage for %s", entity_id) + try: + entity = hass.data[const.DOMAIN][const.SENSOR_PLATFORM][entity_id] + entity.last_collection = dt_util.as_local(last_collection) + entity.update_state() + except KeyError as err: + _LOGGER.error( + "Failed setting last collection for %s - %s", entity_id, err + ) + + hass.data.setdefault(const.DOMAIN, {}) + hass.data[const.DOMAIN].setdefault(const.SENSOR_PLATFORM, {}) + hass.services.async_register( + const.DOMAIN, + "collect_garbage", + handle_collect_garbage, + schema=COLLECT_NOW_SCHEMA, + ) + hass.services.async_register( + const.DOMAIN, + "update_state", + handle_update_state, + schema=UPDATE_STATE_SCHEMA, + ) + hass.services.async_register( + const.DOMAIN, "add_date", handle_add_date, schema=ADD_REMOVE_DATE_SCHEMA + ) + hass.services.async_register( + const.DOMAIN, + "remove_date", + handle_remove_date, + schema=ADD_REMOVE_DATE_SCHEMA, + ) + hass.services.async_register( + const.DOMAIN, "offset_date", handle_offset_date, schema=OFFSET_DATE_SCHEMA + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" + _LOGGER.debug( + "Setting %s (%s) from ConfigFlow", + config_entry.title, + config_entry.options[const.CONF_FREQUENCY], + ) + config_entry.add_update_listener(update_listener) + # Add sensor + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + config_entry, const.SENSOR_PLATFORM + ) + ) + return True + + +async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Handle removal of an entry.""" + try: + await hass.config_entries.async_forward_entry_unload( + config_entry, const.SENSOR_PLATFORM + ) + _LOGGER.info( + "Successfully removed sensor from the garbage_collection integration" + ) + except ValueError: + pass + + +async def async_migrate_entry(_: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.info( + "Migrating %s from version %s", config_entry.title, config_entry.version + ) + new_data: Dict[str, Any] = {**config_entry.data} + new_options: Dict[str, Any] = {**config_entry.options} + removed_data: Dict[str, Any] = {} + removed_options: Dict[str, Any] = {} + _LOGGER.debug("new_data %s", new_data) + _LOGGER.debug("new_options %s", new_options) + if config_entry.version == 1: + to_remove = [ + "offset", + "move_country_holidays", + "holiday_in_week_move", + "holiday_pop_named", + "holiday_move_offset", + "prov", + "state", + "observed", + "exclude_dates", + "include_dates", + ] + for remove in to_remove: + if remove in new_data: + removed_data[remove] = new_data[remove] + del new_data[remove] + if remove in new_options: + removed_options[remove] = new_options[remove] + del new_options[remove] + if new_data.get(const.CONF_FREQUENCY) in const.MONTHLY_FREQUENCY: + if const.CONF_WEEK_ORDER_NUMBER in new_data: + new_data[const.CONF_WEEKDAY_ORDER_NUMBER] = new_data[ + const.CONF_WEEK_ORDER_NUMBER + ] + new_data[const.CONF_FORCE_WEEK_NUMBERS] = True + del new_data[const.CONF_WEEK_ORDER_NUMBER] + else: + new_data[const.CONF_FORCE_WEEK_NUMBERS] = False + _LOGGER.info("Updated data config for week_order_number") + if new_options.get(const.CONF_FREQUENCY) in const.MONTHLY_FREQUENCY: + if const.CONF_WEEK_ORDER_NUMBER in new_options: + new_options[const.CONF_WEEKDAY_ORDER_NUMBER] = new_options[ + const.CONF_WEEK_ORDER_NUMBER + ] + new_options[const.CONF_FORCE_WEEK_NUMBERS] = True + del new_options[const.CONF_WEEK_ORDER_NUMBER] + _LOGGER.info("Updated options config for week_order_number") + else: + new_options[const.CONF_FORCE_WEEK_NUMBERS] = False + if config_entry.version <= 4: + if const.CONF_WEEKDAY_ORDER_NUMBER in new_data: + new_data[const.CONF_WEEKDAY_ORDER_NUMBER] = list( + map(str, new_data[const.CONF_WEEKDAY_ORDER_NUMBER]) + ) + if const.CONF_WEEKDAY_ORDER_NUMBER in new_options: + new_options[const.CONF_WEEKDAY_ORDER_NUMBER] = list( + map(str, new_options[const.CONF_WEEKDAY_ORDER_NUMBER]) + ) + if config_entry.version <= 5: + for conf in [ + const.CONF_FREQUENCY, + const.CONF_ICON_NORMAL, + const.CONF_ICON_TODAY, + const.CONF_ICON_TOMORROW, + const.CONF_MANUAL, + const.CONF_OFFSET, + const.CONF_EXPIRE_AFTER, + const.CONF_VERBOSE_STATE, + const.CONF_FIRST_MONTH, + const.CONF_LAST_MONTH, + const.CONF_COLLECTION_DAYS, + const.CONF_WEEKDAY_ORDER_NUMBER, + const.CONF_FORCE_WEEK_NUMBERS, + const.CONF_WEEK_ORDER_NUMBER, + const.CONF_DATE, + const.CONF_PERIOD, + const.CONF_FIRST_WEEK, + const.CONF_FIRST_DATE, + const.CONF_SENSORS, + const.CONF_VERBOSE_FORMAT, + const.CONF_DATE_FORMAT, + ]: + if conf in new_data: + new_options[conf] = new_data.get(conf) + del new_data[conf] + if ( + const.CONF_EXPIRE_AFTER in new_options + and len(new_options[const.CONF_EXPIRE_AFTER]) == 5 + ): + new_options[const.CONF_EXPIRE_AFTER] = ( + new_options[const.CONF_EXPIRE_AFTER] + ":00" + ) + config_entry.version = const.CONFIG_VERSION + config_entry.data = MappingProxyType({**new_data}) + config_entry.options = MappingProxyType({**new_options}) + if removed_data: + _LOGGER.error( + "Removed data config %s. " + "Please check the documentation how to configure the functionality.", + removed_data, + ) + if removed_options: + _LOGGER.error( + "Removed options config %s. " + "Please check the documentation how to configure the functionality.", + removed_options, + ) + _LOGGER.info( + "%s migration to version %s successful", + config_entry.title, + config_entry.version, + ) + return True + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener - to re-create device after options update.""" + await hass.config_entries.async_forward_entry_unload(entry, const.SENSOR_PLATFORM) + hass.async_add_job( + hass.config_entries.async_forward_entry_setup(entry, const.SENSOR_PLATFORM) + ) diff --git a/custom_components/garbage_collection/calendar.py b/custom_components/garbage_collection/calendar.py new file mode 100644 index 0000000..591df50 --- /dev/null +++ b/custom_components/garbage_collection/calendar.py @@ -0,0 +1,151 @@ +"""Garbage collection calendar.""" +from __future__ import annotations + +from datetime import datetime, timedelta + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import Throttle + +from .const import CALENDAR_NAME, CALENDAR_PLATFORM, DOMAIN, SENSOR_PLATFORM + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) + + +async def async_setup_entry( + _: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + # pylint: disable=unused-argument + """Add calendar entity to HA.""" + async_add_entities([GarbageCollectionCalendar()], True) + + +class GarbageCollectionCalendar(CalendarEntity): + """The garbage collection calendar class.""" + + instances = False + + def __init__(self) -> None: + """Create empty calendar.""" + self._cal_data: dict = {} + self._attr_name = CALENDAR_NAME + GarbageCollectionCalendar.instances = True + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + return self.hass.data[DOMAIN][CALENDAR_PLATFORM].event + + @property + def name(self) -> str | None: + """Return the name of the entity.""" + return self._attr_name + + async def async_update(self) -> None: + """Update all calendars.""" + await self.hass.data[DOMAIN][CALENDAR_PLATFORM].async_update() + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: + """Get all events in a specific time frame.""" + return await self.hass.data[DOMAIN][CALENDAR_PLATFORM].async_get_events( + hass, start_date, end_date + ) + + @property + def extra_state_attributes(self) -> dict | None: + """Return the device state attributes.""" + if self.hass.data[DOMAIN][CALENDAR_PLATFORM].event is None: + # No tasks, we don't need to show anything. + return None + return {} + + +class EntitiesCalendarData: + """Class used by the Entities Calendar class to hold all entity events.""" + + __slots__ = "_hass", "event", "entities", "_throttle" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize an Entities Calendar Data.""" + self._hass = hass + self.event: CalendarEvent | None = None + self.entities: list[str] = [] + + def add_entity(self, entity_id: str) -> None: + """Append entity ID to the calendar.""" + if entity_id not in self.entities: + self.entities.append(entity_id) + + def remove_entity(self, entity_id: str) -> None: + """Remove entity ID from the calendar.""" + if entity_id in self.entities: + self.entities.remove(entity_id) + + async def async_get_events( + self, hass: HomeAssistant, start_datetime: datetime, end_datetime: datetime + ) -> list[CalendarEvent]: + """Get all tasks in a specific time frame.""" + events: list[CalendarEvent] = [] + if SENSOR_PLATFORM not in hass.data[DOMAIN]: + return events + start_date = start_datetime.date() + end_date = end_datetime.date() + for entity in self.entities: + if ( + entity not in hass.data[DOMAIN][SENSOR_PLATFORM] + or hass.data[DOMAIN][SENSOR_PLATFORM][entity].hidden + ): + continue + garbage_collection = hass.data[DOMAIN][SENSOR_PLATFORM][entity] + start = garbage_collection.get_next_date(start_date, True) + while start is not None and start_date <= start <= end_date: + try: + end = start + timedelta(days=1) + except TypeError: + end = start + name = ( + garbage_collection.name + if garbage_collection.name is not None + else "Unknown" + ) + if garbage_collection.expire_after is None: + event = CalendarEvent( + summary=name, + start=start, + end=end, + ) + else: + event = CalendarEvent( + summary=name, + start=datetime.combine(start, datetime.min.time()), + end=datetime.combine(start, garbage_collection.expire_after), + ) + events.append(event) + start = garbage_collection.get_next_date( + start + timedelta(days=1), True + ) + return events + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self) -> None: + """Get the latest data.""" + next_dates = {} + for entity in self.entities: + if self._hass.data[DOMAIN][SENSOR_PLATFORM][entity].next_date is not None: + next_dates[entity] = self._hass.data[DOMAIN][SENSOR_PLATFORM][ + entity + ].next_date + if len(next_dates) > 0: + entity_id = min(next_dates.keys(), key=(lambda k: next_dates[k])) + start = next_dates[entity_id] + end = start + timedelta(days=1) + name = self._hass.data[DOMAIN][SENSOR_PLATFORM][entity_id].name + self.event = CalendarEvent( + summary=name, + start=start, + end=end, + ) diff --git a/custom_components/garbage_collection/config_flow.py b/custom_components/garbage_collection/config_flow.py new file mode 100644 index 0000000..d8cd8c3 --- /dev/null +++ b/custom_components/garbage_collection/config_flow.py @@ -0,0 +1,281 @@ +"""Adds config flow for GarbageCollection.""" +from __future__ import annotations + +import logging + +# import uuid +from collections.abc import Mapping +from typing import Any, Dict, cast + +import voluptuous as vol +from homeassistant.const import ATTR_HIDDEN, CONF_ENTITIES, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, + SchemaFlowMenuStep, + SchemaOptionsFlowHandler, +) + +from . import const, helpers + +_LOGGER = logging.getLogger(__name__) + + +async def _validate_config( + _: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, data: Any +) -> Any: + """Validate config.""" + if const.CONF_DATE in data: + try: + helpers.month_day_text(data[const.CONF_DATE]) + except vol.Invalid as exc: + raise SchemaFlowError("month_day") from exc + return data + + +def required( + key: str, options: Dict[str, Any], default: Any | None = None +) -> vol.Required: + """Return vol.Required.""" + if isinstance(options, dict) and key in options: + suggested_value = options[key] + elif default is not None: + suggested_value = default + else: + return vol.Required(key) + return vol.Required(key, description={"suggested_value": suggested_value}) + + +def optional( + key: str, options: Dict[str, Any], default: Any | None = None +) -> vol.Optional: + """Return vol.Optional.""" + if isinstance(options, dict) and key in options: + suggested_value = options[key] + elif default is not None: + suggested_value = default + else: + return vol.Optional(key) + return vol.Optional(key, description={"suggested_value": suggested_value}) + + +async def general_config_schema( + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, +) -> vol.Schema: + """Generate config schema.""" + return vol.Schema( + { + optional(CONF_NAME, handler.options): selector.TextSelector(), + required( + const.CONF_FREQUENCY, handler.options, const.DEFAULT_FREQUENCY + ): selector.SelectSelector( + selector.SelectSelectorConfig(options=const.FREQUENCY_OPTIONS) + ), + optional( + const.CONF_ICON_NORMAL, handler.options, const.DEFAULT_ICON_NORMAL + ): selector.IconSelector(), + optional( + const.CONF_ICON_TODAY, handler.options, const.DEFAULT_ICON_TODAY + ): selector.IconSelector(), + optional( + const.CONF_ICON_TOMORROW, handler.options, const.DEFAULT_ICON_TOMORROW + ): selector.IconSelector(), + optional(const.CONF_EXPIRE_AFTER, handler.options): selector.TimeSelector(), + optional( + const.CONF_VERBOSE_STATE, handler.options, const.DEFAULT_VERBOSE_STATE + ): bool, + optional(ATTR_HIDDEN, handler.options, False): bool, + optional(const.CONF_MANUAL, handler.options, False): bool, + optional( + const.CONF_MOVE_COUNTRY_HOLIDAYS, + handler.options, + const.DEFAULT_HOLIDAY_IN_WEEK_MOVE, + ): bool, + optional( + const.CONF_HOLIDAY_COUNTRY, + handler.options, + const.DEFAULT_HOLIDAY_COUNTRY, + ): selector.TextSelector(), + } + ) + + +async def general_options_schema( + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, +) -> vol.Schema: + """Generate options schema.""" + return vol.Schema( + { + required( + const.CONF_FREQUENCY, handler.options, const.DEFAULT_FREQUENCY + ): selector.SelectSelector( + selector.SelectSelectorConfig(options=const.FREQUENCY_OPTIONS) + ), + optional( + const.CONF_ICON_NORMAL, handler.options, const.DEFAULT_ICON_NORMAL + ): selector.IconSelector(), + optional( + const.CONF_ICON_TODAY, handler.options, const.DEFAULT_ICON_TODAY + ): selector.IconSelector(), + optional( + const.CONF_ICON_TOMORROW, handler.options, const.DEFAULT_ICON_TOMORROW + ): selector.IconSelector(), + optional(const.CONF_EXPIRE_AFTER, handler.options): selector.TimeSelector(), + optional( + const.CONF_VERBOSE_STATE, handler.options, const.DEFAULT_VERBOSE_STATE + ): bool, + optional(ATTR_HIDDEN, handler.options, False): bool, + optional(const.CONF_MANUAL, handler.options, False): bool, + optional( + const.CONF_MOVE_COUNTRY_HOLIDAYS, + handler.options, + const.DEFAULT_HOLIDAY_IN_WEEK_MOVE, + ): bool, + optional( + const.CONF_HOLIDAY_COUNTRY, + handler.options, + const.DEFAULT_HOLIDAY_COUNTRY, + ): selector.TextSelector(), + } + ) + + +async def detail_config_schema( + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, +) -> vol.Schema: + """Generate options schema.""" + options_schema: Dict[vol.Optional | vol.Required, Any] = {} + if handler.options[const.CONF_FREQUENCY] in const.ANNUAL_FREQUENCY: + # "annual" + options_schema[ + required(const.CONF_DATE, handler.options) + ] = selector.TextSelector() + elif handler.options[const.CONF_FREQUENCY] in const.GROUP_FREQUENCY: + # "group" + options_schema[ + required(CONF_ENTITIES, handler.options) + ] = selector.EntitySelector( + selector.EntitySelectorConfig( + domain="sensor", integration=const.DOMAIN, multiple=True + ), + ) + elif handler.options[const.CONF_FREQUENCY] not in const.BLANK_FREQUENCY: + # everything else except "blank" and every-n-days + if handler.options[const.CONF_FREQUENCY] not in const.DAILY_FREQUENCY: + options_schema[ + required(const.CONF_COLLECTION_DAYS, handler.options) + ] = selector.SelectSelector( + selector.SelectSelectorConfig( + options=const.WEEKDAY_OPTIONS, + multiple=True, + mode=selector.SelectSelectorMode.LIST, + ) + ) + # everything else except "blank" + options_schema[ + optional(const.CONF_FIRST_MONTH, handler.options, const.DEFAULT_FIRST_MONTH) + ] = selector.SelectSelector( + selector.SelectSelectorConfig(options=const.MONTH_OPTIONS) + ) + options_schema[ + optional(const.CONF_LAST_MONTH, handler.options, const.DEFAULT_LAST_MONTH) + ] = selector.SelectSelector( + selector.SelectSelectorConfig(options=const.MONTH_OPTIONS) + ) + if handler.options[const.CONF_FREQUENCY] in const.MONTHLY_FREQUENCY: + # "monthly" + options_schema[ + optional(const.CONF_WEEKDAY_ORDER_NUMBER, handler.options) + ] = selector.SelectSelector( + selector.SelectSelectorConfig( + options=const.ORDER_OPTIONS, + multiple=True, + mode=selector.SelectSelectorMode.LIST, + ) + ) + options_schema[ + optional(const.CONF_FORCE_WEEK_NUMBERS, handler.options) + ] = selector.BooleanSelector() + if handler.options[const.CONF_FREQUENCY] in const.WEEKLY_DAILY_MONTHLY: + # "every-n-weeks", "every-n-days", "monthly" + uom = {"every-n-weeks": "weeks", "every-n-days": "days", "monthly": "month"} + options_schema[ + required(const.CONF_PERIOD, handler.options) + ] = selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, + max=1000, + mode=selector.NumberSelectorMode.BOX, + unit_of_measurement=uom[handler.options[const.CONF_FREQUENCY]], + ) + ) + if handler.options[const.CONF_FREQUENCY] in const.WEEKLY_FREQUENCY_X: + # every-n-weeks + options_schema[ + required( + const.CONF_FIRST_WEEK, handler.options, const.DEFAULT_FIRST_WEEK + ) + ] = selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, + max=52, + mode=selector.NumberSelectorMode.BOX, + unit_of_measurement="weeks", + ) + ) + if handler.options[const.CONF_FREQUENCY] in const.DAILY_FREQUENCY: + # every-n-days + options_schema[ + required(const.CONF_FIRST_DATE, handler.options, helpers.now().date()) + ] = selector.DateSelector() + if handler.options.get(const.CONF_VERBOSE_STATE, False): + # "verbose_state" + options_schema[ + required( + const.CONF_VERBOSE_FORMAT, handler.options, const.DEFAULT_VERBOSE_FORMAT + ) + ] = selector.TextSelector() + options_schema[ + required(const.CONF_DATE_FORMAT, handler.options, const.DEFAULT_DATE_FORMAT) + ] = selector.TextSelector() + return vol.Schema(options_schema) + + +async def choose_details_step(_: dict[str, Any]) -> str: + """Return next step_id for options flow.""" + return "detail" + + +CONFIG_FLOW: Dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(general_config_schema, next_step=choose_details_step), + "detail": SchemaFlowFormStep( + detail_config_schema, validate_user_input=_validate_config + ), +} +OPTIONS_FLOW: Dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(general_options_schema, next_step=choose_details_step), + "detail": SchemaFlowFormStep( + detail_config_schema, validate_user_input=_validate_config + ), +} + + +# mypy: ignore-errors +class GarbageCollectionConfigFlowHandler(SchemaConfigFlowHandler, domain=const.DOMAIN): + """Handle a config or options flow for GarbageCollection.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + VERSION = const.CONFIG_VERSION + + @callback + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title. + + The options parameter contains config entry options, which is the union of user + input from the config flow steps. + """ + return cast(str, options["name"]) if "name" in options else "" diff --git a/custom_components/garbage_collection/const.py b/custom_components/garbage_collection/const.py new file mode 100644 index 0000000..a2aca1d --- /dev/null +++ b/custom_components/garbage_collection/const.py @@ -0,0 +1,144 @@ +"""Define constants used in garbage_collection.""" +from homeassistant.helpers import selector + +# Constants for garbage_collection. +# Base component constants +DOMAIN = "garbage_collection" +CALENDAR_NAME = "Garbage Collection" +SENSOR_PLATFORM = "sensor" +CALENDAR_PLATFORM = "calendar" +ATTRIBUTION = "Data from this is provided by garbage_collection." +CONFIG_VERSION = 6 + +ATTR_NEXT_DATE = "next_date" +ATTR_DAYS = "days" +ATTR_LAST_COLLECTION = "last_collection" +ATTR_LAST_UPDATED = "last_updated" + +# Device classes +BINARY_SENSOR_DEVICE_CLASS = "connectivity" +DEVICE_CLASS = "garbage_collection__schedule" + +# Configuration +CONF_SENSOR = "sensor" +CONF_ENABLED = "enabled" +CONF_FREQUENCY = "frequency" +CONF_MANUAL = "manual_update" +CONF_ICON_NORMAL = "icon_normal" +CONF_ICON_TODAY = "icon_today" +CONF_ICON_TOMORROW = "icon_tomorrow" +CONF_OFFSET = "offset" +CONF_EXPIRE_AFTER = "expire_after" +CONF_VERBOSE_STATE = "verbose_state" +CONF_FIRST_MONTH = "first_month" +CONF_LAST_MONTH = "last_month" +CONF_COLLECTION_DAYS = "collection_days" +CONF_WEEKDAY_ORDER_NUMBER = "weekday_order_number" +CONF_FORCE_WEEK_NUMBERS = "force_week_order_numbers" +CONF_WEEK_ORDER_NUMBER = "week_order_number" # Obsolete +CONF_DATE = "date" +CONF_PERIOD = "period" +CONF_FIRST_WEEK = "first_week" +CONF_FIRST_DATE = "first_date" +CONF_SENSORS = "sensors" +CONF_VERBOSE_FORMAT = "verbose_format" +CONF_DATE_FORMAT = "date_format" +CONF_MOVE_COUNTRY_HOLIDAYS = "move_country_holidays" +CONF_HOLIDAY_COUNTRY = "holiday_country" + +# Defaults +DEFAULT_NAME = DOMAIN +DEFAULT_FIRST_MONTH = "jan" +DEFAULT_LAST_MONTH = "dec" +DEFAULT_FREQUENCY = "weekly" +DEFAULT_PERIOD = 1 +DEFAULT_FIRST_WEEK = 1 +DEFAULT_VERBOSE_STATE = False +DEFAULT_HOLIDAY_IN_WEEK_MOVE = False +DEFAULT_HOLIDAY_COUNTRY = "FR" +DEFAULT_DATE_FORMAT = "%d-%b-%Y" +DEFAULT_VERBOSE_FORMAT = "on {date}, in {days} days" + +# Icons +DEFAULT_ICON_NORMAL = "mdi:trash-can" +DEFAULT_ICON_TODAY = "mdi:delete-restore" +DEFAULT_ICON_TOMORROW = "mdi:delete-circle" +ICON = DEFAULT_ICON_NORMAL + +# States +STATE_TODAY = "today" +STATE_TOMORROW = "tomorrow" + +FREQUENCY_OPTIONS = [ + selector.SelectOptionDict(value="weekly", label="weekly"), + selector.SelectOptionDict(value="even-weeks", label="even-weeks"), + selector.SelectOptionDict(value="odd-weeks", label="odd-weeks"), + selector.SelectOptionDict(value="every-n-weeks", label="every-n-weeks"), + selector.SelectOptionDict(value="every-n-days", label="every-n-days"), + selector.SelectOptionDict(value="monthly", label="monthly"), + selector.SelectOptionDict(value="annual", label="annual"), + selector.SelectOptionDict(value="blank", label="blank"), + selector.SelectOptionDict(value="group", label="group"), +] + +WEEKLY_FREQUENCY = ["weekly", "even-weeks", "odd-weeks"] +EXCEPT_ANNUAL_GROUP = [ + "weekly", + "even-weeks", + "odd-weeks", + "every-n-weeks", + "every-n-days", + "monthly", + "blank", +] +EXCEPT_ANNUAL_GROUP_BLANK = [ + "weekly", + "even-weeks", + "odd-weeks", + "every-n-weeks", + "every-n-days", + "monthly", +] +WEEKLY_DAILY_MONTHLY = ["every-n-weeks", "every-n-days", "monthly"] +WEEKLY_FREQUENCY_X = ["every-n-weeks"] +DAILY_FREQUENCY = ["every-n-days"] +DAILY_BLANK_FREQUENCY = ["blank", "every-n-days"] +MONTHLY_FREQUENCY = ["monthly"] +ANNUAL_GROUP_FREQUENCY = ["annual", "group"] +ANNUAL_FREQUENCY = ["annual"] +GROUP_FREQUENCY = ["group"] +BLANK_FREQUENCY = ["blank"] + +WEEKDAY_OPTIONS = [ + selector.SelectOptionDict(value="mon", label="Monday"), + selector.SelectOptionDict(value="tue", label="Tuesday"), + selector.SelectOptionDict(value="wed", label="Wednesday"), + selector.SelectOptionDict(value="thu", label="Thursday"), + selector.SelectOptionDict(value="fri", label="Friday"), + selector.SelectOptionDict(value="sat", label="Saturday"), + selector.SelectOptionDict(value="sun", label="Sunday"), +] + + +MONTH_OPTIONS = [ + selector.SelectOptionDict(value="jan", label="January"), + selector.SelectOptionDict(value="feb", label="February"), + selector.SelectOptionDict(value="mar", label="March"), + selector.SelectOptionDict(value="apr", label="April"), + selector.SelectOptionDict(value="may", label="May"), + selector.SelectOptionDict(value="jun", label="June"), + selector.SelectOptionDict(value="jul", label="July"), + selector.SelectOptionDict(value="aug", label="August"), + selector.SelectOptionDict(value="sep", label="September"), + selector.SelectOptionDict(value="oct", label="October"), + selector.SelectOptionDict(value="nov", label="November"), + selector.SelectOptionDict(value="dec", label="December"), +] + +ORDER_OPTIONS = [ + selector.SelectOptionDict(value="1", label="1st"), + selector.SelectOptionDict(value="2", label="2nd"), + selector.SelectOptionDict(value="3", label="3rd"), + selector.SelectOptionDict(value="4", label="4th"), + selector.SelectOptionDict(value="5", label="5th"), +] diff --git a/custom_components/garbage_collection/diagnostics.py b/custom_components/garbage_collection/diagnostics.py new file mode 100644 index 0000000..738fc68 --- /dev/null +++ b/custom_components/garbage_collection/diagnostics.py @@ -0,0 +1,29 @@ +"""Diagnostics support for Garbage Collection.""" +from __future__ import annotations + +from typing import Any, Dict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import const + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + entry: ConfigEntry, +) -> Dict[str, Any]: + """Return diagnostics for a config entry.""" + entities = hass.data[const.DOMAIN][const.SENSOR_PLATFORM] + entity_data = [ + entities[entity] + for entity in entities + if entities[entity].unique_id == entry.data["unique_id"] + ][0] + data = { + "entity_id": entity_data.entity_id, + "state": entity_data.state, + "attributes": entity_data.extra_state_attributes, + "config_entry": entry.as_dict(), + } + return data diff --git a/custom_components/garbage_collection/helpers.py b/custom_components/garbage_collection/helpers.py new file mode 100644 index 0000000..1bbd6b1 --- /dev/null +++ b/custom_components/garbage_collection/helpers.py @@ -0,0 +1,67 @@ +"""Set of functions to handle date and text conversion.""" +from __future__ import annotations + +from datetime import date, datetime +from typing import Any + +import homeassistant.util.dt as dt_util +import voluptuous as vol +from dateutil.parser import ParserError, parse + + +def now() -> datetime: + """Return current date and time. Needed for testing.""" + return dt_util.now() + + +def to_date(day: Any) -> date: + """Convert datetime or text to date, if not already datetime. + + Used for the first date for every_n_days (configured as text) + """ + if day is None: + raise ValueError + if isinstance(day, date): + return day + if isinstance(day, datetime): + return day.date() + return date.fromisoformat(day) + + +def parse_datetime(text: str) -> datetime | None: + """Parse text to datetime object.""" + try: + return parse(text) + except (ParserError, TypeError): + return None + + +def dates_to_texts(dates: list[date]) -> list[str]: + """Convert list of dates to texts.""" + converted: list[str] = [] + for record in dates: + try: + converted.append(record.isoformat()) + except ValueError: + continue + return converted + + +def time_text(value: Any) -> str: + """Have to store time as text - datetime is not JSON serialisable.""" + if value is None or value == "": + return "" + try: + return datetime.strptime(value, "%H:%M").time().strftime("%H:%M") + except ValueError as error: + raise vol.Invalid(f"Invalid date: {value}") from error + + +def month_day_text(value: Any) -> str: + """Validate format month/day.""" + if value is None or value == "": + return "" + try: + return datetime.strptime(value, "%m/%d").date().strftime("%m/%d") + except ValueError as error: + raise vol.Invalid(f"Invalid date: {value}") from error diff --git a/custom_components/garbage_collection/manifest.json b/custom_components/garbage_collection/manifest.json new file mode 100644 index 0000000..fbc3ae7 --- /dev/null +++ b/custom_components/garbage_collection/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "garbage_collection", + "name": "Garbage Collection", + "codeowners": [ + "@bruxy70" + ], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/bruxy70/Garbage-Collection/", + "integration_type": "helper", + "iot_class": "calculated", + "issue_tracker": "https://github.com/bruxy70/Garbage-Collection/issues", + "requirements": [ + "python-dateutil>=2.8.2", + "holidays>=0.40" + ], + "version": "3.21-morgane-fr" +} \ No newline at end of file diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py new file mode 100644 index 0000000..bbc1adc --- /dev/null +++ b/custom_components/garbage_collection/sensor.py @@ -0,0 +1,840 @@ +"""Sensor platform for garbage_collection.""" +from __future__ import annotations + +import logging +from datetime import date, datetime, time, timedelta +from typing import Any, Dict, Generator + +from dateutil.relativedelta import relativedelta +import holidays +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_HIDDEN, + CONF_ENTITIES, + CONF_NAME, + WEEKDAYS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity + +from . import const, helpers +from .calendar import EntitiesCalendarData + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=10) +THROTTLE_INTERVAL = timedelta(seconds=60) + + +async def async_setup_entry( + _: HomeAssistant, config_entry: ConfigEntry, async_add_devices: AddEntitiesCallback +) -> None: + """Create garbage collection entities defined in config_flow and add them to HA.""" + frequency = config_entry.options.get(const.CONF_FREQUENCY) + name = ( + config_entry.title + if config_entry.title is not None + else config_entry.data.get(CONF_NAME) + ) + _frequency_function = { + "weekly": WeeklyCollection, + "even-weeks": WeeklyCollection, + "odd-weeks": WeeklyCollection, + "every-n-weeks": WeeklyCollection, + "every-n-days": DailyCollection, + "monthly": MonthlyCollection, + "annual": AnnualCollection, + "group": GroupCollection, + "blank": BlankCollection, + } + if frequency in _frequency_function: + add_devices = _frequency_function[frequency] + async_add_devices([add_devices(config_entry)], True) + else: + _LOGGER.error("(%s) Unknown frequency %s", name, frequency) + raise ValueError + + +class GarbageCollection(RestoreEntity): + """GarbageCollection Sensor class.""" + + __slots__ = ( + "_attr_icon", + "_attr_name", + "_attr_state", + "_collection_dates", + "_date_format", + "_days", + "_first_month", + "_hidden", + "_holiday_country", + "_holidays_calendar", + "_icon_normal", + "_icon_today", + "_icon_tomorrow", + "_last_month", + "_last_updated", + "_manual", + "_next_date", + "_verbose_format", + "_verbose_state", + "config_entry", + "expire_after", + "last_collection", + ) + + def __init__(self, config_entry: ConfigEntry) -> None: + """Read configuration and initialise class variables.""" + config = config_entry.options + self.config_entry = config_entry + self._attr_name = ( + config_entry.title + if config_entry.title is not None + else config.get(CONF_NAME) + ) + self._hidden = config.get(ATTR_HIDDEN, False) + self._manual = config.get(const.CONF_MANUAL) + first_month = config.get(const.CONF_FIRST_MONTH, const.DEFAULT_FIRST_MONTH) + months = [m["value"] for m in const.MONTH_OPTIONS] + self._first_month: int = ( + months.index(first_month) + 1 if first_month in months else 1 + ) + last_month = config.get(const.CONF_LAST_MONTH, const.DEFAULT_LAST_MONTH) + self._last_month: int = ( + months.index(last_month) + 1 if last_month in months else 12 + ) + self._verbose_state = config.get(const.CONF_VERBOSE_STATE) + self._icon_normal = config.get(const.CONF_ICON_NORMAL) + self._icon_today = config.get(const.CONF_ICON_TODAY) + self._icon_tomorrow = config.get(const.CONF_ICON_TOMORROW) + exp = config.get(const.CONF_EXPIRE_AFTER) + self.expire_after: time | None = ( + None + if ( + exp is None + or datetime.strptime(exp, "%H:%M:%S").time() == time(0, 0, 0) + ) + else datetime.strptime(exp, "%H:%M:%S").time() + ) + self._date_format = config.get( + const.CONF_DATE_FORMAT, const.DEFAULT_DATE_FORMAT + ) + self._verbose_format = config.get( + const.CONF_VERBOSE_FORMAT, const.DEFAULT_VERBOSE_FORMAT + ) + self._holiday_country = config.get( + const.CONF_HOLIDAY_COUNTRY, const.DEFAULT_HOLIDAY_COUNTRY + ) + if config.get(const.CONF_MOVE_COUNTRY_HOLIDAYS, False): + try: + self._holidays_calendar = holidays.country_holidays( + self._holiday_country + ) + except NotImplementedError: + _LOGGER.error( + "(%s) Unknown country code for holidays: %s", + self._attr_name, + self._holiday_country, + ) + self._holidays_calendar = None + else: + self._holidays_calendar = None + self._collection_dates: list[date] = [] + self._next_date: date | None = None + self._last_updated: datetime | None = None + self.last_collection: datetime | None = None + self._days: int | None = None + self._attr_state = "" if bool(self._verbose_state) else 2 + self._attr_icon = self._icon_normal + + async def async_added_to_hass(self) -> None: + """When sensor is added to hassio, add it to calendar.""" + await super().async_added_to_hass() + self.hass.data[const.DOMAIN][const.SENSOR_PLATFORM][self.entity_id] = self + + # Restore stored state + if (state := await self.async_get_last_state()) is not None: + self._last_updated = None # Unblock update - after options change + self._attr_state = state.state + self._days = ( + state.attributes[const.ATTR_DAYS] + if const.ATTR_DAYS in state.attributes + else None + ) + next_date = ( + helpers.parse_datetime(state.attributes[const.ATTR_NEXT_DATE]) + if const.ATTR_NEXT_DATE in state.attributes + else None + ) + self._next_date = None if next_date is None else next_date.date() + self.last_collection = ( + helpers.parse_datetime(state.attributes[const.ATTR_LAST_COLLECTION]) + if const.ATTR_LAST_COLLECTION in state.attributes + else None + ) + + # Create device + device_registry = dr.async_get(self.hass) + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers={(const.DOMAIN, self.unique_id)}, + name=self._attr_name, + manufacturer="bruxy70", + ) + + # Create or add to calendar + if not self.hidden: + if const.CALENDAR_PLATFORM not in self.hass.data[const.DOMAIN]: + self.hass.data[const.DOMAIN][ + const.CALENDAR_PLATFORM + ] = EntitiesCalendarData(self.hass) + _LOGGER.debug("Creating garbage_collection calendar") + await self.hass.config_entries.async_forward_entry_setup( + self.config_entry, const.CALENDAR_PLATFORM + ) + + self.hass.data[const.DOMAIN][const.CALENDAR_PLATFORM].add_entity( + self.entity_id + ) + + async def async_will_remove_from_hass(self) -> None: + """When sensor is added to hassio, remove it.""" + await super().async_will_remove_from_hass() + del self.hass.data[const.DOMAIN][const.SENSOR_PLATFORM][self.entity_id] + self.hass.data[const.DOMAIN][const.CALENDAR_PLATFORM].remove_entity( + self.entity_id + ) + + @property + def unique_id(self) -> str: + """Return a unique ID to use for this sensor.""" + if "unique_id" in self.config_entry.data: # From legacy config + return self.config_entry.data["unique_id"] + return self.config_entry.entry_id + + @property + def device_info(self) -> DeviceInfo | None: + """Return device info.""" + return { + "identifiers": {(const.DOMAIN, self.unique_id)}, + "name": self.config_entry.data.get("name"), + "manufacturer": "bruxy70", + } + + @property + def name(self) -> str | None: + """Return the name of the sensor.""" + return self._attr_name + + @property + def next_date(self) -> date | None: + """Return next date attribute.""" + return self._next_date + + @property + def hidden(self) -> bool: + """Return the hidden attribute.""" + return self._hidden + + @property + def native_unit_of_measurement(self) -> str | None: + """Return unit of measurement - None for numerical value.""" + return None + + @property + def native_value(self) -> object: + """Return the state of the sensor.""" + return self._attr_state + + @property + def last_updated(self) -> datetime | None: + """Return when the sensor was last updated.""" + return self._last_updated + + @property + def icon(self) -> str: + """Return the entity icon.""" + return self._attr_icon + + @property + def extra_state_attributes(self) -> Dict[str, Any]: + """Return the state attributes.""" + state_attr = { + const.ATTR_DAYS: self._days, + const.ATTR_LAST_COLLECTION: self.last_collection, + const.ATTR_LAST_UPDATED: self._last_updated, + const.ATTR_NEXT_DATE: None + if self._next_date is None + else datetime( + self._next_date.year, self._next_date.month, self._next_date.day + ).astimezone(), + # Needed for translations to work + ATTR_DEVICE_CLASS: self.DEVICE_CLASS, + } + return state_attr + + @property + def DEVICE_CLASS(self) -> str: # pylint: disable=C0103 + """Return the class of the sensor.""" + return const.DEVICE_CLASS + + def __repr__(self) -> str: + """Return main sensor parameters.""" + return ( + f"{self.__class__.__name__}(name={self._attr_name}, " + f"entity_id={self.entity_id}, " + f"state={self.state}, " + f"attributes={self.extra_state_attributes})" + ) + + def _find_candidate_date(self, day1: date) -> date | None: + """Find the next possible date starting from day1. + + Only based on calendar, not looking at include/exclude days. + Must be implemented for each child class. + """ + raise NotImplementedError + + async def _async_ready_for_update(self) -> bool: + """Check if the entity is ready for the update. + + Skip the update if the sensor was updated today + Except for the sensors with with next date today and after the expiration time + """ + current_date_time = helpers.now() + today = current_date_time.date() + try: + ready_for_update = bool(self._last_updated.date() != today) # type: ignore + except AttributeError: + return True + try: + if self._next_date == today and ( + ( + isinstance(self.expire_after, time) + and current_date_time.time() >= self.expire_after + ) + or ( + isinstance(self.last_collection, datetime) + and self.last_collection.date() == today + ) + ): + return True + except (AttributeError, TypeError): + pass + return ready_for_update + + def date_inside(self, dat: date) -> bool: + """Check if the date is inside first and last date.""" + month = dat.month + if self._first_month <= self._last_month: + return bool(self._first_month <= month <= self._last_month) + return bool(self._first_month <= month or month <= self._last_month) + + def move_to_range(self, day: date) -> date: + """If the date is not in range, move to the range.""" + if not self.date_inside(day): + year = day.year + month = day.month + months = [m["label"] for m in const.MONTH_OPTIONS] + if self._first_month <= self._last_month < month: + _LOGGER.debug( + "(%s) %s outside the range, lookig from %s next year", + self._attr_name, + day, + months[self._first_month - 1], + ) + return date(year + 1, self._first_month, 1) + _LOGGER.debug( + "(%s) %s outside the range, searching from %s", + self._attr_name, + day, + months[self._first_month - 1], + ) + return date(year, self._first_month, 1) + return day + + def collection_schedule( + self, date1: date | None = None, date2: date | None = None + ) -> Generator[date, None, None]: + """Get dates within configured date range.""" + today = helpers.now().date() + first_date: date = date(today.year - 1, 1, 1) if date1 is None else date1 + last_date: date = date(today.year + 1, 12, 31) if date2 is None else date2 + first_date = self.move_to_range(first_date) + while True: + try: + next_date = self._find_candidate_date(first_date) + except (TypeError, ValueError): + return + if next_date is None or next_date > last_date: + return + if (new_date := self.move_to_range(next_date)) != next_date: + first_date = new_date # continue from next year + else: + yield next_date + first_date = next_date + relativedelta(days=1) # look from the next day + + def _shift_for_holiday(self, collection_date: date) -> date: + """Move the date forward if a public holiday falls on it or earlier that week. + + First check Monday..collection_date (inclusive) for a holiday: if any day in + that range is a holiday, move forward by one day. Then keep moving forward, + one day at a time, while the new candidate day is itself a holiday. + """ + if self._holidays_calendar is None: + return collection_date + monday = collection_date - timedelta(days=collection_date.weekday()) + week_has_holiday = any( + (monday + timedelta(days=i)) in self._holidays_calendar + for i in range(collection_date.weekday() + 1) + ) + if not week_has_holiday: + return collection_date + shifted = collection_date + timedelta(days=1) + while shifted in self._holidays_calendar: + shifted = shifted + timedelta(days=1) + _LOGGER.debug( + "(%s) %s shifted to %s because of a public holiday", + self._attr_name, + collection_date, + shifted, + ) + return shifted + + async def _async_load_collection_dates(self) -> None: + """Fill the collection dates list.""" + self._collection_dates.clear() + for collection_date in self.collection_schedule(): + collection_date = self._shift_for_holiday(collection_date) + if collection_date not in self._collection_dates: + self._collection_dates.append(collection_date) + self._collection_dates.sort() + + async def add_date(self, collection_date: date) -> None: + """Add date to _collection_dates.""" + if collection_date not in self._collection_dates: + self._collection_dates.append(collection_date) + self._collection_dates.sort() + else: + _LOGGER.warning( + "%s not added to %s - already on the collection schedule", + collection_date, + self.name, + ) + + async def remove_date(self, collection_date: date) -> None: + """Remove date from _collection dates.""" + try: + self._collection_dates.remove(collection_date) + except ValueError: + _LOGGER.warning( + "%s not removed from %s - not in the collection schedule", + collection_date, + self.name, + ) + + def get_next_date(self, first_date: date, ignore_today=False) -> date | None: + """Get next date from self._collection_dates.""" + current_date_time = helpers.now() + for d in self._collection_dates: # pylint: disable=invalid-name + if d < first_date: + continue + if not ignore_today and d == current_date_time.date(): + expiration = ( + self.expire_after + if self.expire_after is not None + else time(23, 59, 59) + ) + if current_date_time.time() > expiration or ( + self.last_collection is not None + and self.last_collection.date() == current_date_time.date() + and current_date_time.time() >= self.last_collection.time() + ): + continue + return d + return None + + async def async_update(self) -> None: + """Get the latest data and updates the states.""" + if not await self._async_ready_for_update() or not self.hass.is_running: + return + + _LOGGER.debug("(%s) Calling update", self._attr_name) + await self._async_load_collection_dates() + _LOGGER.debug( + "(%s) Dates loaded, firing a garbage_collection_loaded event", + self._attr_name, + ) + event_data = { + "entity_id": self.entity_id, + "collection_dates": helpers.dates_to_texts(self._collection_dates), + } + self.hass.bus.async_fire("garbage_collection_loaded", event_data) + if not self._manual: + self.update_state() + + def update_state(self) -> None: + """Pick the first event from collection dates, update attributes.""" + _LOGGER.debug("(%s) Looking for next collection", self._attr_name) + self._last_updated = helpers.now() + today = self._last_updated.date() + self._next_date = self.get_next_date(today) + if self._next_date is not None: + _LOGGER.debug( + "(%s) next_date (%s), today (%s)", + self._attr_name, + self._next_date, + today, + ) + self._days = (self._next_date - today).days + next_date_txt = self._next_date.strftime(self._date_format) + _LOGGER.debug( + "(%s) Found next collection date: %s, that is in %d days", + self._attr_name, + next_date_txt, + self._days, + ) + if self._days > 1: + if bool(self._verbose_state): + self._attr_state = self._verbose_format.format( + date=next_date_txt, days=self._days + ) + # self._attr_state = "on_date" + else: + self._attr_state = 2 + self._attr_icon = self._icon_normal + else: + if self._days == 0: + if bool(self._verbose_state): + self._attr_state = const.STATE_TODAY + else: + self._attr_state = self._days + self._attr_icon = self._icon_today + elif self._days == 1: + if bool(self._verbose_state): + self._attr_state = const.STATE_TOMORROW + else: + self._attr_state = self._days + self._attr_icon = self._icon_tomorrow + else: + self._days = None + self._attr_state = None + self._attr_icon = self._icon_normal + + +class WeeklyCollection(GarbageCollection): + """Collection every n weeks, odd weeks or even weeks.""" + + __slots__ = "_collection_days", "_first_week", "_period" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Read parameters specific for Weekly Collection Frequency.""" + super().__init__(config_entry) + config = config_entry.options + self._collection_days = config.get(const.CONF_COLLECTION_DAYS, []) + self._period: int + self._first_week: int + frequency = config.get(const.CONF_FREQUENCY) + if frequency == "weekly": + self._period = 1 + self._first_week = 1 + elif frequency == "even-weeks": + self._period = 2 + self._first_week = 2 + elif frequency == "odd-weeks": + self._period = 2 + self._first_week = 1 + else: + self._period = config.get(const.CONF_PERIOD, 1) + self._first_week = config.get(const.CONF_FIRST_WEEK, 1) + + def _find_candidate_date(self, day1: date) -> date | None: + """Calculate possible date, for weekly frequency.""" + week = day1.isocalendar()[1] + weekday = day1.weekday() + offset = -1 + if (week - self._first_week) % self._period == 0: # Collection this week + for day_name in self._collection_days: + day_index = WEEKDAYS.index(day_name) + if day_index >= weekday: # Collection still did not happen + offset = day_index - weekday + break + iterate_by_week = 7 - weekday + WEEKDAYS.index(self._collection_days[0]) + while offset == -1: # look in following weeks + candidate = day1 + relativedelta(days=iterate_by_week) + week = candidate.isocalendar()[1] + if (week - self._first_week) % self._period == 0: + offset = iterate_by_week + break + iterate_by_week += 7 + return day1 + relativedelta(days=offset) + + +class DailyCollection(GarbageCollection): + """Collection every n days.""" + + __slots__ = "_first_date", "_period" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Read parameters specific for Daily Collection Frequency.""" + super().__init__(config_entry) + config = config_entry.options + self._period = config.get(const.CONF_PERIOD) + self._first_date: date | None + try: + self._first_date = helpers.to_date(config.get(const.CONF_FIRST_DATE)) + except ValueError: + self._first_date = None + + def _find_candidate_date(self, day1: date) -> date | None: + """Calculate possible date, for every-n-days frequency.""" + try: + if (day1 - self._first_date).days % self._period == 0: # type: ignore + return day1 + offset = self._period - ( + (day1 - self._first_date).days % self._period # type: ignore + ) + except TypeError as error: + raise ValueError( + f"({self._attr_name}) Please configure first_date and period " + "for every-n-days collection frequency." + ) from error + return day1 + relativedelta(days=offset) + + +class MonthlyCollection(GarbageCollection): + """Collection every nth weekday of each month.""" + + __slots__ = ( + "_collection_days", + "_monthly_force_week_numbers", + "_period", + "_weekday_order_numbers", + "_week_order_numbers", + ) + + def __init__(self, config_entry: ConfigEntry) -> None: + """Read parameters specific for Monthly Collection Frequency.""" + super().__init__(config_entry) + config = config_entry.options + self._collection_days = config.get(const.CONF_COLLECTION_DAYS, []) + self._monthly_force_week_numbers = config.get( + const.CONF_FORCE_WEEK_NUMBERS, False + ) + self._weekday_order_numbers: list + self._week_order_numbers: list + order_numbers: list = [] + if const.CONF_WEEKDAY_ORDER_NUMBER in config: + order_numbers = list(map(int, config[const.CONF_WEEKDAY_ORDER_NUMBER])) + if self._monthly_force_week_numbers: + self._weekday_order_numbers = [] + self._week_order_numbers = order_numbers + else: + self._weekday_order_numbers = order_numbers + self._week_order_numbers = [] + self._period = config.get(const.CONF_PERIOD, 1) + + @staticmethod + def nth_week_date( + week_number: int, date_of_month: date, collection_day: int + ) -> date: + """Find weekday in the nth week of the month.""" + first_of_month = date(date_of_month.year, date_of_month.month, 1) + return first_of_month + relativedelta( + days=collection_day - first_of_month.weekday() + (week_number - 1) * 7 + ) + + @staticmethod + def nth_weekday_date( + weekday_number: int, date_of_month: date, collection_day: int + ) -> date: + """Find nth weekday of the month.""" + first_of_month = date(date_of_month.year, date_of_month.month, 1) + # 1st of the month is before the day of collection + # (so 1st collection week the week when month starts) + if collection_day >= first_of_month.weekday(): + return first_of_month + relativedelta( + days=collection_day + - first_of_month.weekday() + + (weekday_number - 1) * 7 + ) + return first_of_month + relativedelta( + days=7 + - first_of_month.weekday() + + collection_day + + (weekday_number - 1) * 7 + ) + + def _monthly_candidate(self, day1: date) -> date: + """Calculate possible date, for monthly frequency.""" + if self._monthly_force_week_numbers: + for week_order_number in self._week_order_numbers: + candidate_date = MonthlyCollection.nth_week_date( + week_order_number, day1, WEEKDAYS.index(self._collection_days[0]) + ) + # date is today or in the future -> we have the date + if candidate_date >= day1: + return candidate_date + else: + for weekday_order_number in self._weekday_order_numbers: + candidate_date = MonthlyCollection.nth_weekday_date( + weekday_order_number, + day1, + WEEKDAYS.index(self._collection_days[0]), + ) + # date is today or in the future -> we have the date + if candidate_date >= day1: + return candidate_date + if day1.month == 12: + next_collection_month = date(day1.year + 1, 1, 1) + else: + next_collection_month = date(day1.year, day1.month + 1, 1) + if self._monthly_force_week_numbers: + return MonthlyCollection.nth_week_date( + self._week_order_numbers[0], + next_collection_month, + WEEKDAYS.index(self._collection_days[0]), + ) + return MonthlyCollection.nth_weekday_date( + self._weekday_order_numbers[0], + next_collection_month, + WEEKDAYS.index(self._collection_days[0]), + ) + + def _find_candidate_date(self, day1: date) -> date | None: + if self._period is None or self._period == 1: + return self._monthly_candidate(day1) + else: + candidate_date = self._monthly_candidate(day1) + while (candidate_date.month - self._first_month) % self._period != 0: + candidate_date = self._monthly_candidate( + candidate_date + relativedelta(days=1) + ) + return candidate_date + + +class AnnualCollection(GarbageCollection): + """Collection every year.""" + + __slots__ = ("_date",) + + def __init__(self, config_entry: ConfigEntry) -> None: + """Read parameters specific for Annual Collection Frequency.""" + super().__init__(config_entry) + config = config_entry.options + self._date = config.get(const.CONF_DATE) + + def _find_candidate_date(self, day1: date) -> date | None: + """Calculate possible date, for annual frequency.""" + year = day1.year + try: + conf_date = datetime.strptime(self._date, "%m/%d").date() + except TypeError as error: + raise ValueError( + f"({self._attr_name}) Please configure the date " + "for annual collection frequency." + ) from error + if (candidate_date := date(year, conf_date.month, conf_date.day)) < day1: + candidate_date = date(year + 1, conf_date.month, conf_date.day) + return candidate_date + + +class GroupCollection(GarbageCollection): + """Group number of sensors.""" + + __slots__ = ("_entities",) + + def __init__(self, config_entry: ConfigEntry) -> None: + """Read parameters specific for Group Collection Frequency.""" + super().__init__(config_entry) + config = config_entry.options + self._entities = config.get(CONF_ENTITIES, []) + + def _find_candidate_date(self, day1: date) -> date | None: + """Calculate possible date, for group frequency.""" + candidate_date = None + try: + for entity_id in self._entities: + entity: GarbageCollection = self.hass.data[const.DOMAIN][ + const.SENSOR_PLATFORM + ][entity_id] + next_date = entity.get_next_date(day1) + if next_date is not None and ( + candidate_date is None or next_date < candidate_date + ): + candidate_date = next_date + except KeyError as error: + raise ValueError from error + except TypeError as error: + _LOGGER.error("(%s) Please add entities for the group.", self._attr_name) + raise ValueError from error + return candidate_date + + async def _async_ready_for_update(self) -> bool: + """Check if the entity is ready for the update. + + For group sensors wait for update of the sensors in the group + """ + current_date_time = helpers.now() + today = current_date_time.date() + try: + ready_for_update = bool(self._last_updated.date() != today) # type: ignore + except AttributeError: + ready_for_update = True + members_ready = True + for entity_id in self._entities: + try: + entity: GarbageCollection = self.hass.data[const.DOMAIN][ + const.SENSOR_PLATFORM + ][entity_id] + await entity.async_update() + except KeyError: + members_ready = False + break + if (last_updated := entity.last_updated) is None: + ready_for_update = True + continue + # Wait for all members to get updated + if last_updated.date() != today: + members_ready = False + break + # A member got updated after the group update + if self._last_updated is None or last_updated > self._last_updated: + ready_for_update = True + if ready_for_update and not members_ready: + ready_for_update = False + return ready_for_update + + +class BlankCollection(GarbageCollection): + """No collection - for mnual update.""" + + def _find_candidate_date(self, day1: date) -> date | None: + """Do not return any date for blank frequency.""" + return None + + async def _async_load_collection_dates(self) -> None: + """Clear collection dates (filled in by the blueprint).""" + self._collection_dates.clear() + return + + async def async_update(self) -> None: + """Get the latest data and updates the states.""" + if not await self._async_ready_for_update() or not self.hass.is_running: + return + + _LOGGER.debug("(%s) Calling update", self._attr_name) + await self._async_load_collection_dates() + _LOGGER.debug( + "(%s) Dates loaded, firing a garbage_collection_loaded event", + self._attr_name, + ) + event_data = { + "entity_id": self.entity_id, + "collection_dates": [], + } + self.hass.bus.async_fire("garbage_collection_loaded", event_data) diff --git a/custom_components/garbage_collection/services.yaml b/custom_components/garbage_collection/services.yaml new file mode 100644 index 0000000..0eeed9a --- /dev/null +++ b/custom_components/garbage_collection/services.yaml @@ -0,0 +1,60 @@ +collect_garbage: + description: Set the last_collection attribute to the current date and time. + target: + entity: + integration: garbage_collection + fields: + entity_id: + description: The garbage_collection sensor entity_id + example: sensor.general_waste + last_collection: + description: Date and time of the last collection (optional) + example: "2020-08-16 10:54:00" +add_date: + description: Manually add collection date. + target: + entity: + integration: garbage_collection + fields: + entity_id: + description: The garbage_collection sensor entity_id + example: sensor.general_waste + date: + description: Collection date to add + example: '"2020-08-16"' +offset_date: + description: Move the collection date by a number of days. + target: + entity: + integration: garbage_collection + fields: + entity_id: + description: The garbage_collection sensor entity_id + example: sensor.general_waste + date: + description: Collection date to move + example: '"2020-08-16"' + offset: + description: Nuber of days to move (negative number will move it back) + example: 1 +remove_date: + description: Remove automatically calculated collection date. + target: + entity: + integration: garbage_collection + fields: + entity_id: + description: The garbage_collection sensor entity_id + example: sensor.general_waste + date: + description: Collection date to remove + example: '"2020-08-16"' +update_state: + description: Update the entity state and attributes. Used with the manual_update option, do defer the update after changing the automatically created schedule by automation trigered by the garbage_collection_loaded event. + target: + entity: + integration: garbage_collection + fields: + entity_id: + description: The garbage_collection sensor entity_id + example: sensor.general_waste diff --git a/custom_components/garbage_collection/translations/cs.json b/custom_components/garbage_collection/translations/cs.json new file mode 100644 index 0000000..014c23b --- /dev/null +++ b/custom_components/garbage_collection/translations/cs.json @@ -0,0 +1,104 @@ +{ + "config": { + "step": { + "user": { + "title": "Garbage Collection - Konfigurace (1/2)", + "description": "Zadej jméno sensoru a nastav parametry. Více na https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Friendly name", + "hidden": "Skrýt v kalendáři", + "frequency": "Frekvence", + "manual_update": "Manual update - state je aktualizovaný manuálně voláním služby", + "icon_normal": "Ikona (mdi:trash-can)", + "icon_tomorrow": "Ikona svoz zítra (mdi:delete-restore)", + "icon_today": "Ikona svoz dnes (mdi:delete-circle)", + "expire_after": "Čas expirace (HH:MM)", + "verbose_state": "Verbose state (popis místo čísel)" + } + }, + "detail": { + "title": "Garbage Collection - Další parametry (2/2)", + "description": "Více na: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Datum (mm/dd)", + "entities": "Seznam entit (odděleno čárkou)", + "collection_days": "Dny svozu", + "first_month": "První měsíc svozu", + "last_month": "Poslední měsíc svozu", + "period": "Perioda (svoz každých n týdnů/dnů): (1-1000)", + "first_week": "První týden svozu (1-52)", + "first_date": "První datum", + "weekday_order_number": "Pořadí dne v měsící (např. první středa v měsíci)", + "force_week_order_numbers": "Číslo týdne v měsící místo čísla dne", + "verbose_format": "Verbose format (Použij `date` a `days` proměnné (ve složených závorkách))", + "date_format": "Formát data(see http://strftime.org/)" + } + } + }, + "error": { + "value": "Chybná hodnota. Zkontroluj zadané hodnoty!", + "icon": "Ikony musí být zadány ve formátu 'prefix:jmnéno'.", + "days": "Vyber jeden nebo více dní!", + "entities": "Entita neexistuje!", + "month_day": "Špatný formát data!", + "time": "Špatný formát času!", + "weekday_order_number": "Vyber jeden nebo více dní!", + "week_order_number": "Vyber jeden nebo více týdnů!", + "period": "Perioda musí být číslo mezi 1 a 1000", + "first_week": "První týden musí být číslo mezi 1 a 52", + "date": "Špatný formát data!" + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Garbage Collection is allowed." + } + }, + "options": { + "step": { + "init": { + "title": "Garbage Collection - Konfigurace (1/2)", + "description": "Zadej jméno sensoru a nastav parametry. Více na https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Skrýt v kalendáři", + "frequency": "Frekvence", + "manual_update": "State je aktualizovaný manuálně voláním služby", + "icon_normal": "Ikona (mdi:trash-can)", + "icon_tomorrow": "Ikona svoz zítra (mdi:delete-restore)", + "icon_today": "Ikona svoz dnes (mdi:delete-circle)", + "expire_after": "Čas expirace (HH:MM)", + "verbose_state": "Verbose state (popis místo čísel)" + } + }, + "detail": { + "title": "Garbage Collection - Další parametry (2/2)", + "description": "", + "data": { + "date": "Datum (mm/dd)", + "entities": "Seznam entit (odděleno čárkou)", + "collection_days": "Dny svozu", + "first_month": "První měsíc svozu", + "last_month": "Poslední měsíc scozu", + "period": "Perioda (svoz každých n týdnů-dnů): (1-1000)", + "first_week": "První týden svozu (1-52)", + "first_date": "První datum", + "weekday_order_number": "Pořadí dne v měsící (např. první středa v měsíci)", + "force_week_order_numbers": "Číslo týdne v měsící místo čísla dne", + "verbose_format": "Verbose format (Použij `date` a `days` proměnné (ve složených závorkách))", + "date_format": "Formát data(see http://strftime.org/)" + } + } + }, + "error": { + "value": "Chybná hodnota. Zkontroluj zadané hodnoty!", + "icon": "Ikony musí být zadány ve formátu 'prefix:jmnéno'.", + "days": "Vyber jeden nebo více dní!", + "entities": "Entita neexistuje!", + "month_day": "Špatný formát data!", + "time": "Špatný formát času!", + "weekday_order_number": "Vyber jeden nebo více dní!", + "week_order_number": "Vyber jeden nebo více týdnů!", + "period": "Perioda musí být číslo mezi 1 a 1000", + "first_week": "První týden musí být číslo mezi 1 a 52", + "date": "Špatný formát data!" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/da.json b/custom_components/garbage_collection/translations/da.json new file mode 100644 index 0000000..774f026 --- /dev/null +++ b/custom_components/garbage_collection/translations/da.json @@ -0,0 +1,105 @@ +{ + "config": { + "step": { + "user": { + "title": "Garbage Collection - Afhentningsfrekvens (1/2)", + "description": "Angiv navnet på sensoren og konfigurer sensorparametre. For mere info se: https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Visningsnavn", + "hidden": "Skjul i kalender", + "frequency": "Frekvens", + "manual_update": "Manuel opdatering - sensortilstand opdateres manuelt af en service (blueprint)", + "icon_normal": "Ikon (mdi:trash-can) - ikke påkrævet", + "icon_tomorrow": "Ikon ved afhentning i morgen (mdi:delete-restore) - ikke påkrævet", + "icon_today": "Ikon ved afhentning i dag (mdi:delete-circle) - ikke påkrævet", + "expire_after": "Forældes efter (HH:MM) - ikke påkrævet", + "verbose_state": "Verbos tilstand (tekst i stedet for tal)" + } + }, + "detail": { + "title": "Garbage Collection - Yderligere parametre (2/2)", + "description": "For mere info se: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Dato (mm/dd)", + "entities": "Liste af enheder (kommasepareret)", + "collection_days": "Afhentningsdage", + "first_month": "Første afhentningsmåned", + "last_month": "Sidste afhentningsmåned", + "period": "Afhentning sker hver n uger/dage: (1-1000)", + "first_week": "Første afhentningsuge (1-52)", + "first_date": "Første dato", + "weekday_order_number": "Specifikke ugedage i måneden (fx første onsdag i måneden)", + "force_week_order_numbers": "Brug specifik uge i måneden i stedet for specifik ugedag (fx onsdag i første uge i måneden)", + "verbose_format": "Verbost format (anvend `date` og `days` variablerne (i tuborgklammer))", + "date_format": "Datoformat (se http://strftime.org/)" + } + } + }, + "error": { + "value": "Ugyldig værdi. Check dit input!", + "icon": "Ikoner skal angives i formen 'præfiks:navn'.", + "days": "Vælg en eller flere dage!", + "entities": "Enheden findes ikke!", + "month_day": "Ugyldigt dato format!", + "time": "Ugyldigt tids format!", + "weekday_order_number": "Vælg en eller flere dage", + "week_order_number": "Vælg en eller flere uger", + "period": "Afhentningsperioden skal være et tal mellem 1 og 1000", + "first_week": "Første afhentningsuge skal være et tal mellem 1 and 52", + "date": "Ugyldigt dato format!" + }, + "abort": { + "single_instance_allowed": "Det er kun tilladt at have én konfiguration af Garbage Collection." + } + }, + "options": { + "step": { + "init": { + "title": "Garbage Collection - Afhentningsfrekvens (1/2)", + "description": "Ændr sensorparametre. More info on https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Skjul i kalender", + "frequency": "Frekvens", + "manual_update": "Manuel opdatering - sensortilstand opdateres manuelt af en service (blueprint)", + "icon_normal": "Ikon (mdi:trash-can) - ikke påkrævet", + "icon_tomorrow": "Ikon ved afhentning i morgen (mdi:delete-restore) - ikke påkrævet", + "icon_today": "Ikon ved afhentning i dag (mdi:delete-circle) - ikke påkrævet", + "expire_after": "Forældes efter (HH:MM) - ikke påkrævet", + "verbose_state": "Verbos tilstand (tekst i stedet for tal)" + } + }, + "detail": { + "title": "Garbage Collection - Yderligere parametre (2/2)", + "description": "For mere info se: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Dato (mm/dd)", + "entities": "Liste af enheder (kommasepareret)", + "collection_days": "Afhentningsdage", + "first_month": "Første afhentningsmåned", + "last_month": "Sidste afhentningsmåned", + "period": "Afhentning sker hver n uger/dage: (1-1000)", + "first_week": "Første afhentningsuge (1-52)", + "first_date": "Første dato", + "weekday_order_number": "Specifikke ugedage i måneden (fx første onsdag i måneden)", + "force_week_order_numbers": "Brug specifik uge i måneden i stedet for specifik ugedag (fx onsdag i første uge i måneden)", + "verbose_format": "Verbost format (anvend `date` og `days` variablerne (i tuborgklammer))", + "date_format": "Datoformat (se http://strftime.org/)" + } + } + }, + "error": { + "value": "Ugyldig værdi. Check dit input!", + "icon": "Ikoner skal angives i formen 'præfiks:navn'.", + "days": "Vælg en eller flere dage!", + "entities": "Enheden findes ikke!", + "month_day": "Ugyldigt dato format!", + "time": "Ugyldigt tids format!", + "weekday_order_number": "Vælg en eller flere dage", + "week_order_number": "Vælg en eller flere uger", + "period": "Afhentningsperioden skal være et tal mellem 1 og 1000", + "first_week": "Første afhentningsuge skal være et tal mellem 1 and 52", + "date": "Ugyldigt dato format!" + } + } + +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/de.json b/custom_components/garbage_collection/translations/de.json new file mode 100644 index 0000000..0c29fc6 --- /dev/null +++ b/custom_components/garbage_collection/translations/de.json @@ -0,0 +1,104 @@ +{ + "config": { + "step": { + "user": { + "title": "Müllabfuhr - Häufigkeit der Abholung (1/2)", + "description": "Geben Sie den Sensornamen ein und konfigurieren Sie die Sensorparameter. Mehr Informationen unter https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Friendly Name", + "hidden": "Im Kalender ausblenden", + "frequency": "Häufigkeit", + "manual_update": "Manuelle Aktualisierung - Sensorstatus wird manuell durch einen Dienst aktualisiert (Vorlage)", + "icon_normal": "Icon (mdi:trash-can) - optional", + "icon_tomorrow": "Icon Abfuhr morgen (mdi:delete-restore) - optional", + "icon_today": "Icon Abfuhr heute (mdi:delete-circle) - optional", + "expire_after": "Ablauf nach (HH:MM) - optional", + "verbose_state": "Ausführlicher Status (Text, statt Zahl)" + } + }, + "detail": { + "title": "Müllabfuhr - Zusätzliche Parameter (2/2)", + "description": "Mehr Details hier: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Datum (mm/dd)", + "entities": "Liste der Entitäten (durch Komma getrennt)", + "collection_days": "Tage der Abholung", + "first_month": "Erster Abholungsmonat", + "last_month": "Letzter Abholungsmonat", + "period": "Abholung alle n Wochen/Tage: (1-1000)", + "first_week": "Erste Abholungswoche (1-52)", + "first_date": "Erstes Datum", + "weekday_order_number": "Reihenfolge der Wochentage im Monat (z. B. erster Mittwoch im Monat)", + "force_week_order_numbers": "Reihenfolge der Woche im Monat statt Wochentagsreihenfolge (z. B. am Mittwoch der ersten Woche)", + "verbose_format": "Ausführliches Format (unter Verwendung der Variablen 'Datum' und 'Tage' (in eckigen Klammern))", + "date_format": "Datumsformat (siehe http://strftime.org/)" + } + } + }, + "error": { + "value": "Ungültiger Wert. Bitte überprüfen Sie Ihre Eingabe!", + "icon": "Icons sollten in der Form 'Präfix:Name' angegeben werden.", + "days": "Wählen Sie 1 oder mehrere Tage aus!", + "entities": "Entität existiert nicht!", + "month_day": "Ungültiges Datumsformat!", + "time": "Ungültiges Zeitformat!", + "weekday_order_number": "Wählen Sie 1 oder mehrere Tage aus", + "week_order_number": "Wählen Sie 1 oder mehrere Wochen aus", + "period": "Zeitraum muss eine Zahl zwischen 1 und 1000 sein", + "first_week": "Erste Woche muss eine Zahl zwischen 1 und 52 sein", + "date": "Ungültiges Datumsformat!" + }, + "abort": { + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Garbage Collection erlaubt." + } + }, + "options": { + "step": { + "init": { + "title": "Müllabfuhr - Häufigkeit der Abholung (1/2)", + "description": "Sensorparameter ändern. Mehr Informationen unter https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Im Kalender ausblenden", + "frequency": "Häufigkeit", + "manual_update": "Manuelle Aktualisierung - Sensorstatus wird manuell durch einen Dienst aktualisiert (Vorlage)", + "icon_normal": "Icon (mdi:trash-can) - optional", + "icon_tomorrow": "Icon Abfuhr morgen (mdi:delete-restore) - optional", + "icon_today": "Icon Abfuhr heute (mdi:delete-circle) - optional", + "expire_after": "Ablauf nach (HH:MM) - optional", + "verbose_state": "Ausführlicher Status (Text, statt Zahl)" + } + }, + "detail": { + "title": "Müllabfuhr - Zusätzliche Parameter (2/2)", + "description": "Mehr Details hier: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Datum (mm/dd)", + "entities": "Liste der Entitäten (durch Komma getrennt)", + "collection_days": "Tage der Abholung", + "first_month": "Erster Abholungsmonat", + "last_month": "Letzter Abholungsmonat", + "period": "Abholung alle n Wochen/Tage: (1-1000)", + "first_week": "Erste Abholungswoche (1-52)", + "first_date": "Erstes Datum", + "weekday_order_number": "Reihenfolge der Wochentage im Monat (z. B. erster Mittwoch im Monat)", + "force_week_order_numbers": "Reihenfolge der Woche im Monat statt Wochentagsreihenfolge (z. B. am Mittwoch der ersten Woche)", + "verbose_format": "Ausführliches Format (unter Verwendung der Variablen 'Datum' und 'Tage' (in eckigen Klammern))", + "date_format": "Datumsformat (siehe http://strftime.org/)" + } + } + }, + "error": { + "value": "Ungültiger Wert. Bitte überprüfen Sie Ihre Eingabe!", + "icon": "Icons sollten in der Form 'Präfix:Name' angegeben werden.", + "days": "Wählen Sie 1 oder mehrere Tage aus!", + "entities": "Entität existiert nicht!", + "month_day": "Ungültiges Datumsformat!", + "time": "Ungültiges Zeitformat!", + "weekday_order_number": "Wählen Sie 1 oder mehrere Tage aus", + "week_order_number": "Wählen Sie 1 oder mehrere Wochen aus", + "period": "Zeitraum muss eine Zahl zwischen 1 und 1000 sein", + "first_week": "Erste Woche muss eine Zahl zwischen 1 und 52 sein", + "date": "Ungültiges Datumsformat!" + } + } +} diff --git a/custom_components/garbage_collection/translations/en.json b/custom_components/garbage_collection/translations/en.json new file mode 100644 index 0000000..57b97f2 --- /dev/null +++ b/custom_components/garbage_collection/translations/en.json @@ -0,0 +1,108 @@ +{ + "config": { + "step": { + "user": { + "title": "Garbage Collection - Collection frequency (1/2)", + "description": "Enter the sensor name and configure sensor parameters. More info on https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Friendly name", + "hidden": "Hide in calendar", + "frequency": "Frequency", + "manual_update": "Manual update - sensor state updated manually by a service (Blueprint)", + "icon_normal": "Icon (mdi:trash-can) - optional", + "icon_tomorrow": "Icon collection tomorrow (mdi:delete-restore) - optional", + "icon_today": "Icon collection today (mdi:delete-circle) - optional", + "expire_after": "Expire after (HH:MM) - optional", + "verbose_state": "Verbose state (text, instead of number)", + "move_country_holidays": "Shift collection if a public holiday falls in the week", + "holiday_country": "Country code for public holidays (e.g. FR)" + } + }, + "detail": { + "title": "Garbage Collection - Additional parameters (2/2)", + "description": "More details here: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Date (mm/dd)", + "entities": "List of entities (comma separated)", + "collection_days": "Collection days", + "first_month": "First collection month", + "last_month": "Last collection month", + "period": "Collection every n weeks/days: (1-1000)", + "first_week": "First collection week (1-52)", + "first_date": "First date", + "weekday_order_number": "Order of the weekday in the month (e.g. first Wednesday of the month)", + "force_week_order_numbers": "Order of week in a month instead of order of weekday (e.g. on Wednesday of the first week)", + "verbose_format": "Verbose format (using `date` and `days` variables (in squary brackets))", + "date_format": "Date format (see http://strftime.org/)" + } + } + }, + "error": { + "value": "Invalid value. Please check your input!", + "icon": "Icons should be specified in the form 'prefix:name'.", + "days": "Select 1 or more days!", + "entities": "Entity does not exist!", + "month_day": "Invalid date format!", + "time": "Invalid time format!", + "weekday_order_number": "Select 1 or more days", + "week_order_number": "Select 1 or more weeks", + "period": "Period must be a number between 1 and 1000", + "first_week": "First week must be a number between 1 and 52", + "date": "Invalid date format!" + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Garbage Collection is allowed." + } + }, + "options": { + "step": { + "init": { + "title": "Garbage Collection - Collection frequency (1/2)", + "description": "Change sensor parameters. More info on https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Hide in calendar", + "frequency": "Frequency", + "manual_update": "Manual update - sensor state updated manually by a service (Blueprint)", + "icon_normal": "Icon (mdi:trash-can) - optional", + "icon_tomorrow": "Icon collection tomorrow (mdi:delete-restore) - optional", + "icon_today": "Icon collection today (mdi:delete-circle) - optional", + "expire_after": "Expire after (HH:MM) - optional", + "verbose_state": "Verbose state (text, instead of number)", + "move_country_holidays": "Shift collection if a public holiday falls in the week", + "holiday_country": "Country code for public holidays (e.g. FR)" + } + }, + "detail": { + "title": "Garbage Collection - Additional parameters (2/2)", + "description": "More details here: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Date (mm/dd)", + "entities": "List of entities (comma separated)", + "collection_days": "Collection days", + "first_month": "First collection month", + "last_month": "Last collection month", + "period": "Collection every n weeks/days: (1-1000)", + "first_week": "First collection week (1-52)", + "first_date": "First date", + "weekday_order_number": "Order of the weekday in the month (e.g. first Wednesday of the month)", + "force_week_order_numbers": "Order of week in a month instead of order of weekday (e.g. on Wednesday of the first week)", + "verbose_format": "Verbose format (using `date` and `days` variables (in squary brackets))", + "date_format": "Date format (see http://strftime.org/)" + } + } + }, + "error": { + "value": "Invalid value. Please check your input!", + "icon": "Icons should be specified in the form 'prefix:name'.", + "days": "Select 1 or more days!", + "entities": "Entity does not exist!", + "month_day": "Invalid date format!", + "time": "Invalid time format!", + "weekday_order_number": "Select 1 or more days", + "week_order_number": "Select 1 or more weeks", + "period": "Period must be a number between 1 and 1000", + "first_week": "First week must be a number between 1 and 52", + "date": "Invalid date format!" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/es.json b/custom_components/garbage_collection/translations/es.json new file mode 100644 index 0000000..6d65d3d --- /dev/null +++ b/custom_components/garbage_collection/translations/es.json @@ -0,0 +1,105 @@ +{ + "config": { + "step": { + "user": { + "title": "Recolección de basura - Frecuencia de recogida (1/2)", + "description": "Ingrese el nombre del sensor y configure los parámetros del sensor. Más información en https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Nombre amigable", + "hidden": "Esconderse en el calendario", + "frequency": "Frequencia", + "manual_update": "El estado del sensor se actualiza manualmente llamando a un servicio", + "icon_normal": "Icono (mdi:trash-can)", + "icon_tomorrow": "Icono de recoleccion para mañana (mdi:delete-restore)", + "icon_today": "Icono de recoleccion para hoy (mdi:delete-circle)", + "expire_after": "Caduca después (HH:MM)", + "verbose_state": "Estado detallado (texto, en lugar de número)" + } + }, + "detail": { + "title": "Recolección de basura - Parámetros adicionales (2/2)", + "description": "Más detalles aquí: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Fecha (mm/dd)", + "entities": "Lista de entidades (separadas por comas)", + "collection_days": "Días de recogida", + "first_month": "Primer mes de recogida", + "last_month": "Último mes de recogida", + "period": "Recolección cada n semanas / días: (1-1000)", + "first_week": "Primera semana de recolección (1-52)", + "first_date": "Primera fecha", + "weekday_order_number": "Orden del día de la semana en el mes (por ejemplo, primer miércoles del mes)", + "force_week_order_numbers": "Orden de la semana en un mes en lugar de orden del día de la semana (por ejemplo, el miércoles de la primera semana)", + "verbose_format": "Formato detallado (usando las variables `fecha` y` dias` (entre corchetes))", + "date_format": "Formato de fecha(ver en http://strftime.org/)" + } + } + }, + "error": { + "value": "Valor no válido. Por favor revise su entrada!", + "icon": "Los iconos deben especificarse en el formato 'prefix:name'.", + "days": "Seleccione 1 o más días!", + "entities": "La entidad no existe!", + "month_day": "Formato de fecha inválido!", + "time": "Formato de hora inválido!", + "weekday_order_number": "Seleccione 1 o más días", + "week_order_number": "Seleccione 1 o más semanas", + "period": "El período debe ser un número entre 1 y 1000", + "first_week": "La primera semana debe ser un número entre 1 y 52", + "date": "Formato de fecha inválido!" + }, + "abort": { + "single_instance_allowed": "Solo se permite una única configuración de Recolección de basura." + } + }, + "options": { + "step": { + "init": { + "title": "Recolección de basura - Frecuencia de recogida (1/2)", + "description": "Cambiar los parámetros del sensor. Más información en https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Esconderse en el calendario", + "frequency": "Frequencia", + "manual_update": "El estado del sensor se actualiza manualmente llamando a un servicio", + "icon_normal": "Icono (mdi:trash-can)", + "icon_tomorrow": "Icono de recoleccion para mañana (mdi:delete-restore)", + "icon_today": "Icono de recoleccion para hoy (mdi:delete-circle)", + "expire_after": "Caduca después (HH:MM)", + "verbose_state": "Estado detallado (texto, en lugar de número)" + } + }, + "detail": { + "title": "Recolección de basura - Parámetros adicionales (2/2)", + "description": "Más detalles aquí: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Fecha (mm/dd)", + "entities": "Lista de entidades (separadas por comas)", + "collection_days": "Días de recogida", + "first_month": "Primer mes de recogida", + "last_month": "Último mes de recogida", + "period": "Recolección cada n semanas / días: (1-1000)", + "first_week": "Primera semana de recolección (1-52)", + "first_date": "Primera fecha", + "weekday_order_number": "Orden del día de la semana en el mes (por ejemplo, primer miércoles del mes)", + "force_week_order_numbers": "Orden de la semana en un mes en lugar de orden del día de la semana (por ejemplo, el miércoles de la primera semana)", + "verbose_format": "Formato detallado (usando las variables `fecha` y` dias` (entre corchetes))", + "date_format": "Formato de fecha(ver en http://strftime.org/)" + } + } + }, + "error": { + "value": "Valor no válido. Por favor revise su entrada!", + "icon": "Los iconos deben especificarse en el formato 'prefix:name'.", + "days": "Seleccione 1 o más días!", + "entities": "La entidad no existe!", + "month_day": "Formato de fecha inválido!", + "time": "Formato de hora inválido!", + "weekday_order_number": "Seleccione 1 o más días", + "week_order_number": "Seleccione 1 o más semanas", + "period": "El período debe ser un número entre 1 y 1000", + "first_week": "La primera semana debe ser un número entre 1 y 52", + "date": "Formato de fecha inválido!" + } + } + +} diff --git a/custom_components/garbage_collection/translations/et.json b/custom_components/garbage_collection/translations/et.json new file mode 100644 index 0000000..bd68ee3 --- /dev/null +++ b/custom_components/garbage_collection/translations/et.json @@ -0,0 +1,105 @@ +{ + "config": { + "step": { + "user": { + "title": "Prügivedu - tühjendamiste ajad (1/2)", + "description": "Sisesta nduri andmed. Rohkem infot leiab https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Kuvatav nimi", + "hidden": "Ära näita kalendris", + "frequency": "Sagedus", + "manual_update": "Sensor state updated manually by calling a service", + "icon_normal": "Ikoon (mdi:trash-can) - valikuline", + "icon_tomorrow": "Homme on prügivedu ikoon (mdi:delete-restore) - valikuline", + "icon_today": "Täna on prügivedu ikoon (mdi:delete-circle) - valikuline", + "expire_after": "Ajalõpp (HH:MM) - valikuline", + "verbose_state": "Teavitus tekstina (aeg tekstina numbrite asemel)" + } + }, + "detail": { + "title": "Prügivedu - täiendavad sätted (2/2)", + "description": "Vali üks või mitu tühjendamise nädalapäeva. Rohkem teavet: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Kuupäev (mm/dd)", + "entities": "Olemite nimekiri (komadega eraldatud)", + "collection_days": "Tühjendamise nädalapäevad", + "first_month": "Esimene prügiveo kuu", + "last_month": "Viimane prügiveo kuu", + "period": "Prügivedu iga n päeva/nädala tagant: (1-1000)", + "first_week": "Esimese prügiveo nädal (1-52)", + "first_date": "Esimese prügiveo kuupäev", + "weekday_order_number": "Kuu nädalapäevade järjekord (nt kuu esimene kolmapäev)", + "force_week_order_numbers": "nädala järjekord kuus, mitte nädalapäevade järjekord (nt esimese nädala kolmapäeval).", + "verbose_format": "Teavituse formaat (kasutades `date` and `days` muutujaid (kantsulgudes))", + "date_format": "Kuupäeva formaat (vaata http://strftime.org/)" + } + } + }, + "error": { + "value": "Vigane sisestus, palun kontrolli!", + "icon": "Ikoonid tuleb esitada kujul 'prefix:name'.", + "days": "Valige üks või rohkem päevi!", + "entities": "Olem puudub!", + "month_day": "Vigane kuupäeva formaat!", + "time": "Vigane kellaaja formaat!", + "weekday_order_number": "Valige üks või rohkem päevi", + "week_order_number": "Valige üks või rohkem nädalat", + "period": "Välp peab olema number 1 ja 1000 vahel", + "first_week": "Esimene nädal peab olema number 1 ja 52 vahel", + "date": "Vigane kuupäeva formaat!!" + }, + "abort": { + "single_instance_allowed": "Lubatud on ainult üks prügiveo olemi sidumine." + } + }, + "options": { + "step": { + "init": { + "title": "Prügivedu - tühjendamiste ajad (1/2)", + "description": "Sisesta nduri andmed. Rohkem infot leiab https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Ära näita kalendris", + "frequency": "Sagedus", + "manual_update": "Sensor state updated manually by calling a service", + "icon_normal": "Ikoon (mdi:trash-can) - valikuline", + "icon_tomorrow": "Homme on prügivedu ikoon (mdi:delete-restore) - valikuline", + "icon_today": "Täna on prügivedu ikoon (mdi:delete-circle) - valikuline", + "expire_after": "Ajalõpp (HH:MM) - valikuline", + "verbose_state": "Teavitus tekstina (aeg tekstina numbrite asemel)" + } + }, + "detail": { + "title": "Prügivedu - täiendavad sätted (2/2)", + "description": "Vali üks või mitu tühjendamise nädalapäeva. Rohkem teavet: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Kuupäev (mm/dd)", + "entities": "Olemite nimekiri (komadega eraldatud)", + "collection_days": "Tühjendamise nädalapäevad", + "first_month": "Esimene prügiveo kuu", + "last_month": "Viimane prügiveo kuu", + "period": "Prügivedu iga n päeva/nädala tagant: (1-1000)", + "first_week": "Esimese prügiveo nädal (1-52)", + "first_date": "Esimese prügiveo kuupäev", + "weekday_order_number": "Kuu nädalapäevade järjekord (nt kuu esimene kolmapäev)", + "force_week_order_numbers": "nädala järjekord kuus, mitte nädalapäevade järjekord (nt esimese nädala kolmapäeval).", + "verbose_format": "Teavituse formaat (kasutades `date` and `days` muutujaid (kantsulgudes))", + "date_format": "Kuupäeva formaat (vaata http://strftime.org/)" + } + } + }, + "error": { + "value": "Vigane sisestus, palun kontrolli!", + "icon": "Ikoonid tuleb esitada kujul 'prefix:name'.", + "days": "Valige üks või rohkem päevi!", + "entities": "Olem puudub!", + "month_day": "Vigane kuupäeva formaat!", + "time": "Vigane kellaaja formaat!", + "weekday_order_number": "Valige üks või rohkem päevi", + "week_order_number": "Valige üks või rohkem nädalat", + "period": "Välp peab olema number 1 ja 1000 vahel", + "first_week": "Esimene nädal peab olema number 1 ja 52 vahel", + "date": "Vigane kuupäeva formaat!!" + } + } + +} diff --git a/custom_components/garbage_collection/translations/fr.json b/custom_components/garbage_collection/translations/fr.json new file mode 100644 index 0000000..addd89a --- /dev/null +++ b/custom_components/garbage_collection/translations/fr.json @@ -0,0 +1,110 @@ +{ + "config": { + "step": { + "user": { + "title": "Garbage Collection - Fréquence de la collecte (1/2)", + "description": "Définir le nom du capteur et configurer les paramètres du capteur. Plus d'info sur https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Friendly name", + "hidden": "Masquer dans le calendrier", + "frequency": "Fréquence", + "manual_update": "État du capteur mis à jour manuellement en appelant un service", + "icon_normal": "Icône (mdi:trash-can)", + "icon_tomorrow": "Icône pour collecte à jour J+1 (mdi:delete-restore)", + "icon_today": "Icône pour collecte au jour J (mdi:delete-circle)", + "offset": "Décalage (entre -31 et 31 jours)", + "expire_after": "Expire après (HH:MM)", + "verbose_state": "Etat verbeux (texte, au lieu du chiffre)", + "move_country_holidays": "Décaler la collecte si jour férié dans la semaine", + "holiday_country": "Code pays pour les jours fériés (ex: FR)" + } + }, + "detail": { + "title": "Garbage Collection - Paramètres additionnels (2/2)", + "description": "Choisir un ou plusieurs jours de collecte. Plus de détail ici: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Date (mm/dd)", + "entities": "Liste des entités (séparées par une virgule)", + "collection_days": "Jours de collecte", + "first_month": "Premier mois de la collecte", + "last_month": "Dernier mois de la collecte", + "period": "Collecte toutes les n semaines/jours: (1-1000)", + "first_week": "Première semaine de la collecte (1-52)", + "first_date": "Première date", + "weekday_order_number": "Ordre du jour de la semaine dans le mois (par exemple, premier mercredi du mois)", + "force_week_order_numbers": "Nième occurrence du jour de la semaine dans un mois, au lieu du jour de la semaine dans la Nième semaine de chaque mois. Plus d'info sur https://github.com/bruxy70/Garbage-Collection#parameters-for-monthly-collection", + "verbose_format": "Format état verbeux (utilisation des variables `date` et `days` (entre caractères accolade ))", + "date_format": "Format de date (voir http://strftime.org/)" + } + } + }, + "error": { + "value": "Valeur invalide. Veuillez vérifier votre saisie !", + "icon": "Les icônes doivent être spécifiées dans le format 'prefix:name'.", + "days": "Choisir un ou plusieurs jours !", + "entities": "L'entité n'existe pas !", + "month_day": "Format de date invalide !", + "time": "Format d'heure invalide !", + "weekday_order_number": "Choisir un ou plusieurs jours", + "week_order_number": "Choisir une ou plusieurs semaines", + "period": "La semaine doit être un nombre entre 1 et 1000", + "first_week": "La première semaine doit être un nombre entre 1 et 52", + "date": "Format de date invalide !" + }, + "abort": { + "single_instance_allowed": "Une seule configuration de Garbage Collection est autorisée" + } + }, + "options": { + "step": { + "init": { + "title": "Garbage Collection - Fréquence de la collecte (1/2)", + "description": "Modifier les paramètres des capteurs. Plus d'infos sur https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Masquer dans le calendrier", + "frequency": "Fréquence", + "manual_update": "État du capteur mis à jour manuellement en appelant un service", + "icon_normal": "Icône (mdi:trash-can)", + "icon_tomorrow": "Icône pour collecte à jour J+1 (mdi:delete-restore)", + "icon_today": "Icône pour collecte au jour J (mdi:delete-circle)", + "offset": "Décalage (entre -31 et 31 jours)", + "expire_after": "Expire après (HH:MM)", + "verbose_state": "Etat verbeux (texte, au lieu du chiffre)", + "move_country_holidays": "Décaler la collecte si jour férié dans la semaine", + "holiday_country": "Code pays pour les jours fériés (ex: FR)" + } + }, + "detail": { + "title": "Garbage Collection - Paramètres additionnels (2/2)", + "description": "Choisir un ou plusieurs jours de collecte. Plus de détail ici: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Date (mm/dd)", + "entities": "Liste des entités (séparées par une virgule)", + "collection_days": "Jours de collecte", + "first_month": "Premier mois de la collecte", + "last_month": "Dernier mois de la collecte", + "period": "Collecte toutes les n semaines/jours: (1-1000)", + "first_week": "Première semaine de la collecte (1-52)", + "first_date": "Première date", + "weekday_order_number": "Ordre du jour de la semaine dans le mois (par exemple, premier mercredi du mois)", + "force_week_order_numbers": "Nième occurrence du jour de la semaine dans un mois, au lieu du jour de la semaine dans la Nième semaine de chaque mois. Plus d'info sur https://github.com/bruxy70/Garbage-Collection#parameters-for-monthly-collection", + "verbose_format": "Format état verbeux (utilisation des variables `date` et `days` (entre caractères accolade ))", + "date_format": "Format de date (voir http://strftime.org/)" + } + } + }, + "error": { + "value": "Valeur invalide. Veuillez vérifier votre saisie !", + "icon": "Les icônes doivent être spécifiées dans le format 'prefix:name'.", + "days": "Choisir un ou plusieurs jours !", + "entities": "L'entité n'existe pas !", + "month_day": "Format de date invalide !", + "time": "Format d'heure invalide !", + "weekday_order_number": "Choisir un ou plusieurs jours", + "week_order_number": "Choisir une ou plusieurs semaines", + "period": "La semaine doit être un nombre entre 1 et 1000", + "first_week": "La première semaine doit être un nombre entre 1 et 52", + "date": "Format de date invalide !" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/it.json b/custom_components/garbage_collection/translations/it.json new file mode 100644 index 0000000..418c020 --- /dev/null +++ b/custom_components/garbage_collection/translations/it.json @@ -0,0 +1,105 @@ +{ + "config": { + "step": { + "user": { + "title": "Raccolta Differenziata - Frequenza di raccolta (1/2)", + "description": "Immetti il nome del sensore e configura i suoi parametri. Maggiori informazioni su https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Nome personalizzato", + "hidden": "Nascondi nel calendario", + "frequency": "Frequenza", + "manual_update": "Stato del sensore aggiornato manualmente chiamando un servizio", + "icon_normal": "Icona (mdi:trash-can)", + "icon_tomorrow": "Icona raccolta domani (mdi:delete-restore)", + "icon_today": "Icona raccolta oggi (mdi:delete-circle)", + "expire_after": "Scade dopo (HH:MM) - opzionale", + "verbose_state": "Stato Verbale (testo, al posto di numeri)" + } + }, + "detail": { + "title": "Raccolta Differenziata - Parametri aggiuntivi (2/2)", + "description": "Seleziona uno o più giorni di raccolta. Maggiori dettagli qui: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Data (mm/dd)", + "entities": "Lista di entità (separate da virgole)", + "collection_days": "Giorni di raccolta", + "first_month": "Prima raccolta del mese", + "last_month": "Ultima raccolta del mese", + "period": "Raccolta ogni n settimane/giorno: (1-1000)", + "first_week": "Prima raccolta della settimana (1-52)", + "first_date": "Prima date", + "weekday_order_number": "Ordine del giorno della settimana nel mese (ad esempio il primo mercoledì del mese)", + "force_week_order_numbers": "Ordine della settimana in un mese invece dell'ordine del giorno della settimana (per esempio il mercoledì della prima settimana)", + "verbose_format": "Formato Verbale (usare le variabili `date` e `days` (in parentesi graffe))", + "date_format": "Formato Data (vedere http://strftime.org/)" + } + } + }, + "error": { + "value": "Valore non valido. Per favore controlla i tuoi input!", + "icon": "Le icone devono essere specificate nel formato 'prefix:name'.", + "days": "Seleziona uno o più giorni!", + "entities": "L'entità non esiste!", + "month_day": "Formato data non valido!", + "time": "Formato ora non valido!", + "weekday_order_number": "Seleziona uno o più giorni", + "week_order_number": "Seleziona uno o più settimani", + "period": "Il periodo deve essere un numero compreso tra 1 e 1000", + "first_week": "La prima settimana deve essere un numero tra 1 e 52", + "date": "Formato data non valido!" + }, + "abort": { + "single_instance_allowed": "E' consentita solo una singola configurazione di Raccolta Differenziata." + } + }, + "options": { + "step": { + "init": { + "title": "Raccolta Differenziata - Frequenza di raccolta (1/2)", + "description": "Cambia i parametri del sensore. Maggiori informazioni su https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Nascondi nel calendario", + "frequency": "Frequenza", + "manual_update": "Stato del sensore aggiornato manualmente chiamando un servizio", + "icon_normal": "Icona (mdi:trash-can)", + "icon_tomorrow": "Icona raccolta domani (mdi:delete-restore)", + "icon_today": "Icona raccolta oggi (mdi:delete-circle)", + "expire_after": "Scade dopo (HH:MM) - opzionale", + "verbose_state": "Stato Verbale (testo, al posto di numeri)" + } + }, + "detail": { + "title": "Raccolta Differenziata - Parametri aggiuntivi (2/2)", + "description": "Seleziona uno o più giorni di raccolta. Maggiori dettagli qui: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Data (mm/dd)", + "entities": "Lista di entità (separate da virgole)", + "collection_days": "Giorni di raccolta", + "first_month": "Prima raccolta del mese", + "last_month": "Ultima raccolta del mese", + "period": "Raccolta ogni n settimane/giorno: (1-1000)", + "first_week": "Prima raccolta della settimana (1-52)", + "first_date": "Prima date", + "weekday_order_number": "Ordine del giorno della settimana nel mese (ad esempio il primo mercoledì del mese)", + "force_week_order_numbers": "Ordine della settimana in un mese invece dell'ordine del giorno della settimana (per esempio il mercoledì della prima settimana)", + "verbose_format": "Formato Verbale (usare le variabili `date` e `days` (in parentesi graffe))", + "date_format": "Formato Data (vedere http://strftime.org/)" + } + } + }, + "error": { + "value": "Valore non valido. Per favore controlla i tuoi input!", + "icon": "Le icone devono essere specificate nel formato 'prefix:name'.", + "days": "Seleziona uno o più giorni!", + "entities": "L'entità non esiste!", + "month_day": "Formato data non valido!", + "time": "Formato ora non valido!", + "weekday_order_number": "Seleziona uno o più giorni", + "week_order_number": "Seleziona uno o più settimani", + "period": "Il periodo deve essere un numero compreso tra 1 e 1000", + "first_week": "La prima settimana deve essere un numero tra 1 e 52", + "date": "Formato data non valido!" + } + } + +} diff --git a/custom_components/garbage_collection/translations/pl.json b/custom_components/garbage_collection/translations/pl.json new file mode 100644 index 0000000..3f283c5 --- /dev/null +++ b/custom_components/garbage_collection/translations/pl.json @@ -0,0 +1,105 @@ +{ + "config": { + "step": { + "user": { + "title": "Wywóz Śmieci - Częstotliwość wywozu (1/2)", + "description": "Wprowadź nazwę dla sensora i skonfiguruj jego parametry. Więcej informacji na stronie https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Przyjazna nazwa", + "hidden": "Ukryj w kalendarzu", + "frequency": "Częstotliwość", + "manual_update": "Stan czujnika aktualizowany ręcznie poprzez wywołanie usługi", + "icon_normal": "Ikona (mdi:trash-can) - opcjonalnie", + "icon_tomorrow": "Ikona wywozu jutro (mdi:delete-restore) - opcjonalnie", + "icon_today": "Ikona wywozu dzisiaj (mdi:delete-circle) - opcjonalnie", + "expire_after": "Wygasają po (HH:MM) - opcjonalnie", + "verbose_state": "Tryb gadatliwy (tekst, zamiast liczb)" + } + }, + "detail": { + "title": "Wywóz Śmieci - (2/2)", + "description": "Wybierz jeden lub więcej dni wywozu. Więcej szczegółów tutaj: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Data (mm/dd)", + "entities": "Lista encji (rozdzielane przecinkami)", + "collection_days": "Dni wywozu", + "first_month": "Miesiąc pierwszego wywozu", + "last_month": "Miesiąc ostatniego wywozu", + "period": "Wywóz co n dni/tygodni: (1-1000)", + "first_week": "Tydzień pierwszego wywozu: (1-52)", + "first_date": "Pierwsza data", + "weekday_order_number": "Kolejność dni tygodnia w miesiącu (np. pierwsza środa miesiąca)", + "force_week_order_numbers": "Kolejność tygodni w miesiącu zamiast kolejności dni tygodnia (np. w środę pierwszego tygodnia)", + "verbose_format": "Format trybu gadatliwego (używa zmiennych `date` oraz `days` (w nawiasach klamerkowych))", + "date_format": "Format daty (patrz http://strftime.org/)" + } + } + }, + "error": { + "value": "Niewłaściwa wartość. Sprawdź swoje dane wejściowe!", + "icon": "Ikony powinny być określone w formacie 'prefix:name'.", + "days": "Wybierz 1 lub więcej dni!", + "entities": "Encja nie istnieje!", + "month_day": "Nieprawidłowy format daty!", + "time": "Nieprawidłowy format czasu!", + "weekday_order_number": "Wybierz 1 lub więcej dni!", + "week_order_number": "Wybierz 1 lub więcej tygodni!", + "period": "Okres musi być liczbą od 1 do 1000.", + "first_week": "Pierwszy tydzień musi być liczbą między 1 a 52.", + "date": "Nieprawidłowy format daty!" + }, + "abort": { + "single_instance_allowed": "Tylko jedna konfiguracja dla wywozu śmieci jest dozwolona." + } + }, + "options": { + "step": { + "init": { + "title": "Wywóz Śmieci - Częstotliwośc wywozu (1/2)", + "description": "Zmień parametry sensora. Więcej informacji tutaj https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Ukryj w kalendarzu", + "frequency": "Częstotliwość", + "manual_update": "Stan czujnika aktualizowany ręcznie poprzez wywołanie usługi", + "icon_normal": "Ikona (mdi:trash-can) - opcjonalnie", + "icon_tomorrow": "Ikona wywozu jutro (mdi:delete-restore) - opcjonalnie", + "icon_today": "Ikona wywozu dzisiaj (mdi:delete-circle) - opcjonalnie", + "expire_after": "Wygasają po (HH:MM) - opcjonalnie", + "verbose_state": "Tryb gadatliwy (tekst, zamiast liczb)" + } + }, + "detail": { + "title": "Wywóz Śmieci - Dodatkowe parametry (2/2)", + "description": "Dołączone i wyłączone daty są listą, rozdzielanych przecinkami, dat w formacie rrrr-mm-dd. Więcej informacji tutaj https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Data (mm/dd)", + "entities": "Lista encji (rozdzielane przecinkami)", + "collection_days": "Dni wywozu", + "first_month": "Miesiąc pierwszego wywozu", + "last_month": "Miesiąc ostatniego wywozu", + "period": "Wywóz co n dni/tygodni: (1-1000)", + "first_week": "Tydzień pierwszego wywozu: (1-52)", + "first_date": "Pierwsza data", + "weekday_order_number": "Kolejność dni tygodnia w miesiącu (np. pierwsza środa miesiąca)", + "force_week_order_numbers": "Kolejność tygodni w miesiącu zamiast kolejności dni tygodnia (np. w środę pierwszego tygodnia)", + "verbose_format": "Format trybu gadatliwego (używa zmiennych `date` oraz `days` (w nawiasach klamrowych))", + "date_format": "Format daty (patrz http://strftime.org/)" + } + } + }, + "error": { + "value": "Niewłaściwa wartość. Sprawdź swoje dane wejściowe!", + "icon": "Ikony powinny być określone w formacie 'prefix:name'.", + "days": "Wybierz 1 lub więcej dni!", + "entities": "Encja nie istnieje!", + "month_day": "Nieprawidłowy format daty!", + "time": "Nieprawidłowy format czasu!", + "weekday_order_number": "Wybierz 1 lub więcej dni!", + "week_order_number": "Wybierz 1 lub więcej tygodni!", + "period": "Okres musi być liczbą od 1 do 1000.", + "first_week": "Pierwszy tydzień musi być liczbą między 1 a 52.", + "date": "Nieprawidłowy format daty!" + } + } + +} diff --git a/custom_components/garbage_collection/translations/pt-BR.json b/custom_components/garbage_collection/translations/pt-BR.json new file mode 100644 index 0000000..8475e88 --- /dev/null +++ b/custom_components/garbage_collection/translations/pt-BR.json @@ -0,0 +1,105 @@ +{ + "config": { + "step": { + "user": { + "title": "Coleta de lixo - Frequência de coleta (1/2)", + "description": "Insira o nome do sensor e configure os parâmetros do sensor. Mais informações em https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Nome fantasia", + "hidden": "Ocultar no calendário", + "frequency": "Frequência", + "manual_update": "Atualização manual - estado do sensor atualizado manualmente por um serviço (Blueprint)", + "icon_normal": "Ícone (mdi:trash-can) - opcional", + "icon_tomorrow": "ícones para coleta de amanhã (mdi:delete-restore) - opcional", + "icon_today": "Ícones para coleta de hoje (mdi:delete-circle) - opcional", + "expire_after": "Expirar depois (HH:MM) - opcional", + "verbose_state": "Estado detalhado (texto, em vez de número)" + } + }, + "detail": { + "title": "Coleta de lixo - Parâmetros adicionais (2/2)", + "description": "Mais detalhes aqui: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Data (mm/dd)", + "entities": "Lista de entidades (separadas por vírgulas)", + "collection_days": "Dias de coleta", + "first_month": "Primeiro mês de coletah", + "last_month": "Último mês de coleta", + "period": "Coleta a cada n semanas/dias: (1-1000)", + "first_week": "Primeira semana de coleta (1-52)", + "first_date": "Primeira data", + "weekday_order_number": "Ordem do dia da semana no mês (por exemplo, primeira quarta-feira do mês)", + "force_week_order_numbers": "Ordem da semana em um mês em vez da ordem do dia da semana (por exemplo, na quarta-feira da primeira semana)", + "verbose_format": "Formato detalhado (usando variáveis ​​`date` e `days` (entre colchetes))", + "date_format": "Formato de data (consulte http://strftime.org/)" + } + } + }, + "error": { + "value": "Valor inválido. Por favor, verifique sua entrada!", + "icon": "Os ícones devem ser especificados no formato 'prefix:name'.", + "days": "Selecione 1 ou mais dias!", + "entities": "A entidade não existe!", + "month_day": "Formato de data inválido!", + "time": "Formato de hora inválido!", + "weekday_order_number": "Selecione 1 ou mais dias", + "week_order_number": "Selecione 1 ou mais semanas", + "period": "O período deve ser um número entre 1 e 1000", + "first_week": "A primeira semana deve ser um número entre 1 e 52", + "date": "Formato de data inválido!" + }, + "abort": { + "single_instance_allowed": "Apenas uma única configuração de Coleta de Lixo é permitida." + } + }, + "options": { + "step": { + "init": { + "title": "Coleta de lixo - Frequência de coleta (1/2)", + "description": "Altere os parâmetros do sensor. Mais informações em https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Ocultar no calendário", + "frequency": "Frequência", + "manual_update": "Atualização manual - estado do sensor atualizado manualmente por um serviço (Blueprint)", + "icon_normal": "Ícone (mdi:trash-can) - opcional", + "icon_tomorrow": "Ícones para coleta de amanhã (mdi:delete-restore) - opcional", + "icon_today": "Ícones para coleta de hoje (mdi:delete-circle) - opcional", + "expire_after": "Expirar depois (HH:MM) - opcional", + "verbose_state": "Estado detalhado (texto, em vez de número)" + } + }, + "detail": { + "title": "Coleta de lixo - Parâmetros adicionais (2/2)", + "description": "Mais detalhes aqui: https://github.com/bruxy70/Garbage-Collection", + "data": { + "date": "Data (mm/dd)", + "entities": "Lista de entidades (separadas por vírgulas)", + "collection_days": "Dias de coleta", + "first_month": "Primeiro mês de coleta", + "last_month": "Último mês de coleta", + "period": "Coleta a cada n semanas/dias: (1-1000)", + "first_week": "Primeira semana de coleta (1-52)", + "first_date": "Primeira data", + "weekday_order_number": "Ordem do dia da semana no mês (por exemplo, primeira quarta-feira do mês)", + "force_week_order_numbers": "Ordem da semana em um mês em vez da ordem do dia da semana (por exemplo, na quarta-feira da primeira semana)", + "verbose_format": "Formato detalhado (usando variáveis ​​`date` e `days` (entre colchetes))", + "date_format": "Formato de data (consulte http://strftime.org/)" + } + } + }, + "error": { + "value": "Valor inválido. Por favor, verifique sua entrada!", + "icon": "Os ícones devem ser especificados no formato 'prefix:name'.", + "days": "Selecione 1 ou mais dias!", + "entities": "A entidade não existe!", + "month_day": "Formato de data inválido!", + "time": "Formato de hora inválido!", + "weekday_order_number": "Selecione 1 ou mais dias", + "week_order_number": "Selecione 1 ou mais semanas", + "period": "O período deve ser um número entre 1 e 1000", + "first_week": "A primeira semana deve ser um número entre 1 e 52", + "date": "Formato de data inválido!" + } + } + +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.cs.json b/custom_components/garbage_collection/translations/sensor.cs.json new file mode 100644 index 0000000..15363a8 --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.cs.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Dnes", + "tomorrow": "Zítra" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.da.json b/custom_components/garbage_collection/translations/sensor.da.json new file mode 100644 index 0000000..2141243 --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.da.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "I dag", + "tomorrow": "I morgen" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.de.json b/custom_components/garbage_collection/translations/sensor.de.json new file mode 100644 index 0000000..c69923a --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.de.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Heute", + "tomorrow": "Morgen" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.en.json b/custom_components/garbage_collection/translations/sensor.en.json new file mode 100644 index 0000000..2d945d2 --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.en.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Today", + "tomorrow": "Tomorrow" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.es.json b/custom_components/garbage_collection/translations/sensor.es.json new file mode 100644 index 0000000..a02a09f --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.es.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Hoy", + "tomorrow": "Mañana" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.et.json b/custom_components/garbage_collection/translations/sensor.et.json new file mode 100644 index 0000000..dbec4c5 --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.et.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Täna", + "tomorrow": "Homme" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.fr.json b/custom_components/garbage_collection/translations/sensor.fr.json new file mode 100644 index 0000000..d385466 --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.fr.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Aujourd'hui", + "tomorrow": "Demain" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.it.json b/custom_components/garbage_collection/translations/sensor.it.json new file mode 100644 index 0000000..4864e4f --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.it.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Oggi", + "tomorrow": "Domani" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.nl.json b/custom_components/garbage_collection/translations/sensor.nl.json new file mode 100644 index 0000000..79cec1e --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.nl.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Vandaag", + "tomorrow": "Morgen" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.no.json b/custom_components/garbage_collection/translations/sensor.no.json new file mode 100644 index 0000000..9474f7d --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.no.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "I morgen", + "tomorrow": "I dag" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.pl.json b/custom_components/garbage_collection/translations/sensor.pl.json new file mode 100644 index 0000000..dbc7007 --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.pl.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Dzisiaj", + "tomorrow": "Jutro" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.pt-BR.json b/custom_components/garbage_collection/translations/sensor.pt-BR.json new file mode 100644 index 0000000..e83d158 --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.pt-BR.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Hoje", + "tomorrow": "Amanhã" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.se.json b/custom_components/garbage_collection/translations/sensor.se.json new file mode 100644 index 0000000..0097804 --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.se.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Idag", + "tomorrow": "Imorgon" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.sk.json b/custom_components/garbage_collection/translations/sensor.sk.json new file mode 100644 index 0000000..b228ece --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.sk.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Dnes", + "tomorrow": "Zajtra" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sensor.sl.json b/custom_components/garbage_collection/translations/sensor.sl.json new file mode 100644 index 0000000..70b5643 --- /dev/null +++ b/custom_components/garbage_collection/translations/sensor.sl.json @@ -0,0 +1,8 @@ +{ + "state": { + "garbage_collection__schedule": { + "today": "Danes", + "tomorrow": "Jutri" + } + } +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sk.json b/custom_components/garbage_collection/translations/sk.json new file mode 100644 index 0000000..c4a0302 --- /dev/null +++ b/custom_components/garbage_collection/translations/sk.json @@ -0,0 +1,179 @@ +{ + "config": { + "step": { + "user": { + "title": "Garbage Collection - Nastavenia", + "description": "Zadej meno senzoru a nastav parametre. Viac na https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Friendly name", + "hidden": "Skryť v kalendári", + "frequency": "Frekvencia", + "icon_normal": "Ikona (mdi:trash-can)", + "icon_tomorrow": "Ikona zvoz zajtra (mdi:delete-restore)", + "icon_today": "Ikona zvoz dnes (mdi:delete-circle)", + "offset": "Odsadiť (medzi -31 a 31 dňami)", + "expire_after": "Čas expirácie (HH:MM)", + "verbose_state": "Verbose state (popis miesto čísiel)", + "verbose_format": "Verbose format (Použi `date` a `days` premenné (v zložených zátvorkách))", + "include_dates": "Pridané dátumy (yyyy-mm-dd, yyyy-mm-dd, ...) - voliteľné", + "exclude_dates": "Zakázané dátumy (yyyy-mm-dd, yyyy-mm-dd, ...) - voliteľné", + "date_format": "Formát data(see http://strftime.org/)" + } + }, + "annual_group": { + "title": "Garbage Collection - Ďalšie parametre", + "description": "Zadej dátum zvozu (napr. 11/20 pre 20. november)", + "data": { + "date": "Dátum (mm/dd)", + "entities": "Zoznam entít (oddelené čiarkou)" + } + }, + "detail": { + "title": "Garbage Collection - Dny svozu", + "description": "Vyber jeden alebo viacej dní zvozu. Viac na: https://github.com/bruxy70/Garbage-Collection", + "data": { + "collection_days_mon": "Pondelok", + "collection_days_tue": "Útorok", + "collection_days_wed": "Streda", + "collection_days_thu": "Štvrtok", + "collection_days_fri": "Piatok", + "collection_days_sat": "Sobota", + "collection_days_sun": "Nedeľa", + "force_week_order_numbers": "Číslo týždňa v mesiaci namiesto čísla dňa" + } + }, + "final": { + "title": "Garbage Collection - Ďalšie parametre", + "description": "Pridané a zakázané dátumy sú zoznamy dátumov vo formáte yyyy-mm-dd, oddelených čiarkou. Viac info na https://github.com/bruxy70/Garbage-Collection", + "data": { + "first_month": "Prvý mesiac zvozu", + "last_month": "Posledný mesiac zvozu", + "period": "Perióda (zvoz každých n týždňov/dní): (1-1000)", + "first_week": "Prvý týždeň zvozu (1-52)", + "first_date": "Prvý dátum", + "weekday_order_number_1": "Prvý deň v mesiaci", + "weekday_order_number_2": "Druhý deň v mesiaci", + "weekday_order_number_3": "Tretí deň v mesiaci", + "weekday_order_number_4": "Štvrtý deň v mesiaci", + "weekday_order_number_5": "Piaty deň v mesiaci", + "week_order_number_1": "Prvý týžden v mesiaci", + "week_order_number_2": "Druhý týžden v mesiaci", + "week_order_number_3": "Tretí týžden v mesiaci", + "week_order_number_4": "Štvrtý týžden v mesiaci", + "week_order_number_5": "Piaty týžden v mesiaci", + "move_country_holidays": "Štátne sviatky - krajiny (voliteľné)", + "holiday_move_offset": "Posunúť sviatok o (dní: -7..7)", + "holiday_pop_named": "Ignorovať sviatky (volitelné)", + "holiday_in_week_move": "Posunúť na ďalší deň ak je dovolenka v týždni (voliteľné)", + "prov": "Štátne sviatky - provincie (volitelné)", + "state": "Štátne sviatky - štát (volitelné)", + "observed": "Štátne sviatky - sledované (volitelné)" + + } + } + }, + "error": { + "value": "Chybná hodnota. Skontroluj zadané hodnoty!", + "icon": "Ikony musia býť zadané vo formáte 'prefix:meno'.", + "days": "Vyber jeden nebo viacej dní!", + "entities": "Entita neexistuje!", + "month_day": "Zlý formát dátumu!", + "time": "Zlý formát času!", + "weekday_order_number": "Vyber jeden nebo viacej dní!", + "week_order_number": "Vyber jeden nebo viacej týždňov!", + "period": "Perióda musí být číslo medzi 1 a 1000", + "first_week": "Prvý týždeň musí být číslo medzi 1 a 52", + "date": "Zlý formát dátumu!" + }, + "abort": { + "single_instance_allowed": "Je povolená iba jedna konfigurácia Garbage Collection." + } + }, + "options": { + "step": { + "init": { + "title": "Garbage Collection - Nastavenia", + "description": "Zadaj meno senzora a nastav parametre. Viac na https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Skryť v kalendári", + "frequency": "Frekvencia", + "icon_normal": "Ikona (mdi:trash-can)", + "icon_tomorrow": "Ikona zvoz zajtra (mdi:delete-restore)", + "icon_today": "Ikona zvoz dnes (mdi:delete-circle)", + "offset": "Odsadit (medzi -31 a 31 dny)", + "expire_after": "Čas expirácie (HH:MM)", + "verbose_state": "Verbose state (popis miesto čísiel)", + "verbose_format": "Verbose format (Použi `date` a `days` premenné (v zložených zátvorkách))", + "include_dates": "Pridané dátumy (yyyy-mm-dd, yyyy-mm-dd, ...) - voliteľné", + "exclude_dates": "Zakázané dátumy (yyyy-mm-dd, yyyy-mm-dd, ...) - voliteľné", + "date_format": "Formát dátumu(see http://strftime.org/)" + } + }, + "annual_group": { + "title": "Garbage Collection - Ďalšie parametre", + "description": "Zadej dátum zvozu (napr. 11/20 pre 20. november)", + "data": { + "date": "Dátum (mm/dd)", + "entities": "Zoznam entít (oddelené čiarkou)" + } + }, + "detail": { + "title": "Garbage Collection - Dni zvozu", + "description": "Vyber jeden alebo viac dní zvozu. Více na: https://github.com/bruxy70/Garbage-Collection", + "data": { + "collection_days_mon": "Pondelok", + "collection_days_tue": "Útorok", + "collection_days_wed": "Streda", + "collection_days_thu": "Štvrtok", + "collection_days_fri": "Piatok", + "collection_days_sat": "Sobota", + "collection_days_sun": "Nedeľa", + "force_week_order_numbers": "Číslo týždna v mesiaci namiesto čísla dňa" + + } + }, + "final": { + "title": "Garbage Collection - Ďalšie parametre", + "description": "Pridané a zakázané dátumy sú zoznamy dátumov vo formáte yyyy-mm-dd, oddelených čiarkou. Viac info na https://github.com/bruxy70/Garbage-Collection", + "data": { + "first_month": "Prvý mesiac zvozu", + "last_month": "Posledný mesiac zvozu", + "period": "Perióda (zvoz každých n týždňov/dní): (1-1000)", + "first_week": "Prvý týždeň zvozu (1-52)", + "first_date": "Prvý dátum", + "weekday_order_number_1": "Prvý deň v mesiaci", + "weekday_order_number_2": "Druhý deň v mesiaci", + "weekday_order_number_3": "Tretí deň v mesiaci", + "weekday_order_number_4": "Štvrtý deň v mesiaci", + "weekday_order_number_5": "Piaty deň v mesiaci", + "week_order_number_1": "Prvý týžden v mesiaci", + "week_order_number_2": "Druhý týžden v mesiaci", + "week_order_number_3": "Tretí týžden v mesiaci", + "week_order_number_4": "Štvrtý týžden v mesiaci", + "week_order_number_5": "Piaty týžden v mesiaci", + "move_country_holidays": "Štátne sviatky - krajiny (voliteľné)", + "holiday_move_offset": "Posunúť sviatok o (dní: -7..7)", + "holiday_pop_named": "Ignorovať sviatky (volitelné)", + "holiday_in_week_move": "Posunúť na ďalší deň ak je dovolenka v týždni (voliteľné)", + "prov": "Štátne sviatky - provincie (volitelné)", + "state": "Štátne sviatky - štát (volitelné)", + "observed": "Štátne sviatky - sledované (volitelné)" + } + } + }, + "error": { + "value": "Chybná hodnota. Skontroluj zadané hodnoty!", + "icon": "Ikony musia býť zadané vo formáte 'prefix:meno'.", + "days": "Vyber jeden nebo viacej dní!", + "entities": "Entita neexistuje!", + "month_day": "Zlý formát dátumu!", + "time": "Zlý formát času!", + "weekday_order_number": "Vyber jeden nebo viacej dní!", + "week_order_number": "Vyber jeden nebo viacej týždňov!", + "period": "Perióda musí být číslo medzi 1 a 1000", + "first_week": "Prvý týždeň musí být číslo medzi 1 a 52", + "date": "Zlý formát dátumu!" + } + } + +} \ No newline at end of file diff --git a/custom_components/garbage_collection/translations/sl.json b/custom_components/garbage_collection/translations/sl.json new file mode 100644 index 0000000..592e71d --- /dev/null +++ b/custom_components/garbage_collection/translations/sl.json @@ -0,0 +1,179 @@ +{ + "config": { + "step": { + "user": { + "title": "Odvoz odpadkov - Pogostost odvoza", + "description": "Vpišite ime senzorja in nastavite parametre. Več informacij na https://github.com/bruxy70/Garbage-Collection", + "data": { + "name": "Prijazno ime", + "hidden": "Skrij v koledarju", + "frequency": "Pogostost", + "manual_update": "Stanje senzorja se posodablja ročno s klicem storitve", + "icon_normal": "Ikona (mdi:trash-can) - opcijsko", + "icon_tomorrow": "Ikona pobiranja jutri (mdi:delete-restore) - opcijsko", + "icon_today": "Ikona pobiranja danes (mdi:delete-circle) - opcijsko", + "offset": "Zamik (med -31 in 31 dni)", + "expire_after": "Poteče po (HH:MM) - opcijsko", + "verbose_state": "Verbose stanje (tekst namesto številk)", + "verbose_format": "Verbose format (uporaba spremenljivk `datum` in `dni` (v oglatih oklepajih))", + "include_dates": "Vključi datume (yyyy-mm-dd, yyyy-mm-dd, ...) - opcijsko", + "exclude_dates": "Izključi datume (yyyy-mm-dd, yyyy-mm-dd, ...) - opcijsko", + "date_format": "Format datuma (glej http://strftime.org/)" + } + }, + "annual_group": { + "title": "Odvoz odpadkov - Dodatni parametri", + "description": "Vpišite datum odvoza (npr. 11/20 za 20.November)", + "data": { + "date": "Datum (mm/dd)", + "entities": "Seznam vpisov (ločeno z vejico)" + } + }, + "detail": { + "title": "Odvoz odpadkov - Collection days", + "description": "Izberite enega ali več dni odvoza. Več podrobnosti tukaj: https://github.com/bruxy70/Garbage-Collection", + "data": { + "collection_days_mon": "Ponedeljek", + "collection_days_tue": "Torek", + "collection_days_wed": "Sreda", + "collection_days_thu": "Četrtek", + "collection_days_fri": "Petek", + "collection_days_sat": "Sobota", + "collection_days_sun": "Nedelja", + "force_week_order_numbers": "Naročilo odvoza v tednu meseca namesto v dnevu tedna" + } + }, + "final": { + "title": "Odvoz odpadkov - Dodatni parametri", + "description": "Vključeni in izključeni datumi so seznami datumov, ločeni z vejico v formatu yyyy-mm-dd. Podrobnosti na https://github.com/bruxy70/Garbage-Collection", + "data": { + "first_month": "Prvi mesec odvoza", + "last_month": "Zadnji mesec odvoza", + "period": "Odvoz vsakih v tednov/dni: (1-1000)", + "first_week": "Prvi teden odvoza (1-52)", + "first_date": "Prvi datum", + "weekday_order_number_1": "1. dan tedna v mesecu", + "weekday_order_number_2": "2. dan tedna v mesecu", + "weekday_order_number_3": "3. dan tedna v mesecu", + "weekday_order_number_4": "4. dan tedna v mesecu", + "weekday_order_number_5": "5. dan tedna v mesecu", + "week_order_number_1": "1. teden v mesecu", + "week_order_number_2": "2. teden v mesecu", + "week_order_number_3": "3. teden v mesecu", + "week_order_number_4": "4. teden v mesecu", + "week_order_number_5": "5. teden v mesecu", + "move_country_holidays": "Premakni na naslednji dan, če je praznik (opcijsko)", + "holiday_move_offset": "Premakni praznike za # dni(-7..7)", + "holiday_pop_named": "Ignoriraj praznike (seznam) - opcijsko", + "holiday_in_week_move": "Premakni na naslednji dan, če je praznik v tednu (opcijsko)", + "prov": "Državni prazniki - provincialni (opcijsko)", + "state": "Državni prazniki - državni (opcijsko)", + "observed": "Državni prazniki - premični (opcijsko)" + } + } + }, + "error": { + "value": "Neveljavna vrednost. Prosim, preverite vaš vnos!", + "icon": "Ikone morajo biti vpisane v formatu 'predpona:ime'.", + "days": "Iizberite enega ali več dni!", + "entities": "Entiteta ne obstaja!", + "month_day": "Neveljaven format datuma!", + "time": "Neveljaven format časa!", + "weekday_order_number": "Izberite enega ali več dni", + "week_order_number": "Izberite enega ali več tednov", + "period": "Interval mora biti številka med 1 in 1000", + "first_week": "Prvi teden mora biti številka med 1 in 52", + "date": "Neveljaven format datuma!" + }, + "abort": { + "single_instance_allowed": "Dovoljena je samo ena konfiguracija odvoza odpadkov." + } + }, + "options": { + "step": { + "init": { + "title": "Odvoz odpadkov - Pogostost odvoza", + "description": "Spremenite parametre. Več informacij: https://github.com/bruxy70/Garbage-Collection", + "data": { + "hidden": "Skrij v koledarju", + "frequency": "Pogostost", + "manual_update": "Stanje senzorja se posodablja ročno s klicanjem storitve", + "icon_normal": "Ikona (mdi:trash-can) - opcijsko", + "icon_tomorrow": "Ikona codvoz jutri (mdi:delete-restore) - opcijsko", + "icon_today": "Ikona odvoz danes (mdi:delete-circle) - opcijsko", + "offset": "Zamik (med -31 in 31 dni)", + "expire_after": "Expire after (HH:MM) - opcijsko", + "verbose_state": "Verbose stanje (tekst namesto številk)", + "verbose_format": "Verbose format (uporaba spremenljivk `datum` in `dni` (v oglatih oklepajih))", + "include_dates": "Vključi datume (yyyy-mm-dd, yyyy-mm-dd, ...) - opcijsko", + "exclude_dates": "Izključi datume (yyyy-mm-dd, yyyy-mm-dd, ...) - opcijsko", + "date_format": "Format datume (glej http://strftime.org/)" + } + }, + "annual_group": { + "title": "Odvoz odpadkov - Dodatni parametri", + "description": "Vpišite datum dneva odvoza (npr. 11/20 za 20.November)", + "data": { + "date": "Datum (mm/dd)", + "entities": "Seznam entitet (ločenih z vejivo)" + } + }, + "detail": { + "title": "Odvoz odpadkov - Collection days", + "description": "Izberite enega ali več dni odvoza. Več informacij tukaj: https://github.com/bruxy70/Garbage-Collection", + "data": { + "collection_days_mon": "Ponedeljek", + "collection_days_tue": "Torek", + "collection_days_wed": "Sreda", + "collection_days_thu": "Četrtek", + "collection_days_fri": "Petek", + "collection_days_sat": "Sobota", + "collection_days_sun": "Nedelja", + "force_week_order_numbers": "Naročilo odvoza v tednu meseca namesto v dnevu tedna" + } + }, + "final": { + "title": "Odvoz odpadkov - Dodatni parametri", + "description": "Vključeni in izključeni datumi so seznami datumov, ločeni z vejico v formatu yyyy-mm-dd. Podrobnosti na https://github.com/bruxy70/Garbage-Collection", + "data": { + "first_month": "Prvi mesec odvoza", + "last_month": "Zdnji mesec odvoza", + "period": "Odvoz vsakih n tednov/dni: (1-1000)", + "first_week": "Prvi teden odvoza (1-52)", + "first_date": "Prvi datum", + "weekday_order_number_1": "1. dan tedna v mesecu", + "weekday_order_number_2": "2. dan tedna v mesecu", + "weekday_order_number_3": "3. dan tedna v mesecu", + "weekday_order_number_4": "4. dan tedna v mesecu", + "weekday_order_number_5": "5. dan tedna v mesecu", + "week_order_number_1": "1. teden v mesecu", + "week_order_number_2": "2. teden v mesecu", + "week_order_number_3": "3. teden v mesecu", + "week_order_number_4": "4. teden v mesecu", + "week_order_number_5": "5. teden v mesecu", + "move_country_holidays": "Premakni praznik na naslednji dan (opcijsko)", + "holiday_move_offset": "Premakni praznike za # dni(-7..7)", + "holiday_pop_named": "Ignoriraj praznike (seznam) - opcijsko", + "holiday_in_week_move": "Premakni na naslednji dan, če je praznik v tednu (opcijsko)", + "prov": "Državni prazniki - provincialni (opcijsko)", + "state": "Državni prazniki- državni (opcijsko)", + "observed": "Državni prazniki - premični (opcijsko)" + } + } + }, + "error": { + "value": "Neveljavna vrednost. Prosim, preverite vaš vnos!", + "icon": "Ikona mora biti vpisana v formatu 'predpona:ime'.", + "days": "Izberite enega ali več dni!", + "entities": "Entiteta ne obstaja!", + "month_day": "Neveljaven format datuma!", + "time": "Neveljaven format časa!", + "weekday_order_number": "Izberite enega ali več dni", + "week_order_number": "Izberite enega ali več tednov", + "period": "Interval mora biti številka med 1 in 1000", + "first_week": "Prvi teden mora biti številka med 1 in 52", + "date": "Neveljaven format datuma!" + } + } + +} \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..aea0f17 --- /dev/null +++ b/hacs.json @@ -0,0 +1,5 @@ +{ + "name": "Garbage Collection (fork Morgane - FR + jours feries)", + "domains": ["sensor"], + "homeassistant": "2022.12.0b0" +} diff --git a/info.md b/info.md new file mode 100644 index 0000000..2eb9166 --- /dev/null +++ b/info.md @@ -0,0 +1,76 @@ +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) [![Garbage-Collection](https://img.shields.io/github/v/release/bruxy70/Garbage-Collection.svg?1)](https://github.com/bruxy70/Garbage-Collection) ![Maintenance](https://img.shields.io/maintenance/yes/2022.svg) + +[![Buy me a coffee](https://img.shields.io/static/v1.svg?label=Buy%20me%20a%20coffee&message=🥨&color=black&logo=buy%20me%20a%20coffee&logoColor=white&labelColor=6f4e37)](https://www.buymeacoffee.com/3nXx0bJDP) + +{% if prerelease %} + +### NB!: This is a Beta version! + +{% endif %} + +# Garbage Collection + +The `garbage_collection` component is a Home Assistant custom helper for scheduling/monitoring regular garbage collection. The helper can be configured for weekly schedule (including multiple collection days), bi-weekly (in even or odd weeks), monthly schedule (nth day each month), or annual (e.g. birthdays). You can also configure seasonal calendars (e.g. for bio-waste collection) by configuring the first and last month. You can also group entities, which will merge multiple schedules into one sensor. + +## Examples + +### Images (picture-entity) + + + +### List view (entities) + + + +### Icon view (glance) + + + +### Garbage Collection custom card + + + +Look to the repository for examples of Lovelace configuration. + +## Configuration + +Go to `Settings`/`Devices & Services`/`Helpers`, click on the `+ CREATE HELPER` button, select `Garbage Collection` and configure the helper.
If you would like to add more than one collection schedule, click on the `+ CREATE HELPER` button again and add another `Garbage Collection` helper instance. + +The configuration via `configuration.yaml` has been deprecated. If you have previously configured the integration there, it will be imported to ConfigFlow, and you should remove it. + +For the configuration documentation check the repository file + +## STATE AND ATTRIBUTES + +### State + +The state can be one of + +| Value | Meaning | +| :---- | ---------------------- | +| 0 | Collection is today | +| 1 | Collection is tomorrow | +| 2 | Collection is later | + +If the `verbose_state` parameter is set, it will show the date, and remaining days. For example "Today" or "Tomorrow" or "on 2019-09-10, in 2 days" + +### Attributes + +| Attribute | Description | +| :---------------- | ---------------------------------------- | +| `next_date` | The date of next collection | +| `days` | Days till the next collection | +| `last_collection` | The date and time of the last collection | + +## Services + +### garbage_collection.collect_garbage + +If the collection is scheduled for today, mark it completed and look for the next collection. +It will set the `last_collection` attribute to the current date and time. + +| Attribute | Description | +| :---------- | -------------------------------------------------------------- | +| `entity_id` | The garbage collection entity id (e.g. `sensor.general_waste`) | + +For more details see the repository. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f96d166 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-dateutil>=2.8.2 +types-python-dateutil>=2.8.18 diff --git a/requirements_tests.txt b/requirements_tests.txt new file mode 100644 index 0000000..28f7cc7 --- /dev/null +++ b/requirements_tests.txt @@ -0,0 +1,5 @@ +python-dateutil>=2.8.2 +types-python-dateutil>=2.8.18 +homeassistant>=2023.3.1 +pytest-homeassistant-custom-component>=0.13.5 +aiohttp_cors>=0.7.0