From cf502a11405e65db7f8fb0e5254d1042136d35fd Mon Sep 17 00:00:00 2001 From: Vaclav Date: Sun, 2 Jan 2022 15:50:42 +0100 Subject: [PATCH 01/30] Loading collection dates into a list --- Garbage Collection.code-workspace | 6 +- .../garbage_collection/calendar.py | 5 +- .../garbage_collection/sensor.py | 86 +++++++++++++------ 3 files changed, 68 insertions(+), 29 deletions(-) diff --git a/Garbage Collection.code-workspace b/Garbage Collection.code-workspace index 876a149..bccea69 100644 --- a/Garbage Collection.code-workspace +++ b/Garbage Collection.code-workspace @@ -4,5 +4,9 @@ "path": "." } ], - "settings": {} + "settings": { + "files.associations": { + "*.yaml": "home-assistant" + } + } } \ No newline at end of file diff --git a/custom_components/garbage_collection/calendar.py b/custom_components/garbage_collection/calendar.py index 67ebea0..c30d5bc 100644 --- a/custom_components/garbage_collection/calendar.py +++ b/custom_components/garbage_collection/calendar.py @@ -94,8 +94,7 @@ async def async_get_events(self, hass, start_datetime, end_datetime): ): continue garbage_collection = hass.data[DOMAIN][SENSOR_PLATFORM][entity] - await garbage_collection.async_load_holidays(start_date) - start = await garbage_collection.async_find_next_date(start_date, True) + start = await garbage_collection.async_next_date(start_date, True) while start is not None and start >= start_date and start <= end_date: try: end = start + timedelta(days=1) @@ -126,7 +125,7 @@ async def async_get_events(self, hass, start_datetime, end_datetime): "allDay": False, } events.append(event) - start = await garbage_collection.async_find_next_date( + start = await garbage_collection.async_next_date( start + timedelta(days=1), True ) return events diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index bc3c852..a6ffb47 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -181,6 +181,7 @@ def __init__(self, hass, config, title=None): self._first_date = to_date(config.get(CONF_FIRST_DATE)) except ValueError: self._first_date = None + self._collection_dates = [] self._next_date = None self._last_updated = None self.last_collection = None @@ -475,7 +476,7 @@ async def _async_find_candidate_date(self, day1: date) -> date: try: for entity_id in self._entities: entity = self.hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] - d = await entity.async_find_next_date(day1) + d = await entity.async_next_date(day1) if candidate_date is None or d < candidate_date: candidate_date = d except KeyError: @@ -611,7 +612,7 @@ def move_to_range(self, day: date) -> date: return date(year, self._first_month, 1) return day - async def async_find_next_date(self, first_date: date, ignore_today=False): + async def _async_find_next_date(self, first_date: date) -> Optional[date]: """Get date within configured date range.""" # Today's collection can be triggered by past collection with offset if self._holiday_in_week_move: @@ -641,20 +642,6 @@ async def async_find_next_date(self, first_date: date, ignore_today=False): # Date is before starting date if next_date < first_date: next_date = None - # Today's expiration - now = dt_util.now() - if not ignore_today and next_date == now.date(): - expiration = ( - self.expire_after - if self.expire_after is not None - else time(23, 59, 59) - ) - if now.time() > expiration or ( - self.last_collection is not None - and self.last_collection.date() == now.date() - and now.time() >= self.last_collection.time() - ): - next_date = None # Remove exclude dates if next_date in self._exclude_dates: _LOGGER.debug( @@ -665,6 +652,49 @@ async def async_find_next_date(self, first_date: date, ignore_today=False): next_date = self._insert_include_date(first_date, next_date) return next_date + async def _async_load_collection_dates(self) -> None: + """Fill the collection dates list""" + today = dt_util.now().date() + start_date = end_date = date(today.year - 1, 1, 1) + end_date = date(today.year + 1, 12, 31) + + self._collection_dates.clear() + try: + await asyncio.wait_for(self.async_load_holidays(today), timeout=10) + d = await self._async_find_next_date(start_date) + except asyncio.TimeoutError: + _LOGGER.error("(%s) Timeout loading collection dats", self._name) + return + + while d is not None and d >= start_date and d <= end_date: + self._collection_dates.append(d) + d = await self._async_find_next_date(d + timedelta(days=1)) + self._collection_dates.sort() + + async def async_next_date( + self, first_date: date, ignore_today=False + ) -> Optional[date]: + """Get next date from self._collection_dates""" + + now = dt_util.now() + for d in self._collection_dates: + if d < first_date: + continue + if not ignore_today and d == now.date(): + expiration = ( + self.expire_after + if self.expire_after is not None + else time(23, 59, 59) + ) + if now.time() > expiration or ( + self.last_collection is not None + and self.last_collection.date() == now.date() + and now.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(): @@ -672,20 +702,26 @@ async def async_update(self) -> None: _LOGGER.debug("(%s) Calling update", self._name) now = dt_util.now() today = now.date() + await self._async_load_collection_dates() + _LOGGER.debug("(%s) Dates loaded, waiting for event handlers", self._name) + + """ + TO DO + """ + + _LOGGER.debug( + "(%s) Event haldlers finished, looking for next collection", self._name + ) + self._next_date = await self.async_next_date(today) self._last_updated = now - try: - await asyncio.wait_for(self.async_load_holidays(today), timeout=10) - self._next_date = await asyncio.wait_for( - self.async_find_next_date(today), timeout=10 - ) - except asyncio.TimeoutError: - _LOGGER.error("(%s) Timeout looking for the new date", self._name) - self._next_date = None if self._next_date is not None: + _LOGGER.debug( + "(%s) next_date (%s), today (%s)", self._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 date: %s, that is in %d days", + "(%s) Found next collection date: %s, that is in %d days", self._name, next_date_txt, self._days, From 94189a0df69c51f1b3118ca2d7efcbd8dc199156 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Sun, 2 Jan 2022 16:10:37 +0100 Subject: [PATCH 02/30] Fire event --- custom_components/garbage_collection/sensor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index a6ffb47..40c4719 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -653,7 +653,7 @@ async def _async_find_next_date(self, first_date: date) -> Optional[date]: return next_date async def _async_load_collection_dates(self) -> None: - """Fill the collection dates list""" + """Fill the collection dates list.""" today = dt_util.now().date() start_date = end_date = date(today.year - 1, 1, 1) end_date = date(today.year + 1, 12, 31) @@ -674,8 +674,7 @@ async def _async_load_collection_dates(self) -> None: async def async_next_date( self, first_date: date, ignore_today=False ) -> Optional[date]: - """Get next date from self._collection_dates""" - + """Get next date from self._collection_dates.""" now = dt_util.now() for d in self._collection_dates: if d < first_date: @@ -708,6 +707,11 @@ async def async_update(self) -> None: """ TO DO """ + event_data = { + "device_id": self.entity_id, + "type": "dates_loaded", + } + self.hass.bus.async_fire("garbage_collection", event_data) _LOGGER.debug( "(%s) Event haldlers finished, looking for next collection", self._name From c8de19e0c56a051083575a4d5a634db8ffa9eb6b Mon Sep 17 00:00:00 2001 From: Vaclav Date: Sun, 2 Jan 2022 17:45:06 +0100 Subject: [PATCH 03/30] converted event data --- .../garbage_collection/sensor.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index 40c4719..5dcb051 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -139,6 +139,17 @@ def to_dates(dates: List[Any]) -> List[date]: return converted +def dates_to_texts(dates: List[date]) -> List[str]: + """Convert list of dates to texts""" + converted = [] # type: List[str] + for day in dates: + try: + converted.append(day.isoformat()) + except ValueError: + continue + return converted + + class GarbageCollection(RestoreEntity): """GarbageCollection Sensor class.""" @@ -702,16 +713,17 @@ async def async_update(self) -> None: now = dt_util.now() today = now.date() await self._async_load_collection_dates() - _LOGGER.debug("(%s) Dates loaded, waiting for event handlers", self._name) + _LOGGER.debug("(%s) Dates loaded, firing a garbage_collection_loaded event", self._name) """ TO DO """ event_data = { - "device_id": self.entity_id, - "type": "dates_loaded", + "entity_id": self.entity_id, + "collection_dates": dates_to_texts(self._collection_dates) } - self.hass.bus.async_fire("garbage_collection", event_data) + self.hass.bus.async_fire("garbage_collection_loaded", event_data) + # self.hass.bus.fire("garbage_collection_loaded", event_data) _LOGGER.debug( "(%s) Event haldlers finished, looking for next collection", self._name From fc5a37333b52a0bdc62660e7f36b3360f4669f57 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Sun, 2 Jan 2022 17:45:36 +0100 Subject: [PATCH 04/30] black --- custom_components/garbage_collection/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index 5dcb051..57cc989 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -713,14 +713,16 @@ async def async_update(self) -> None: now = dt_util.now() today = now.date() await self._async_load_collection_dates() - _LOGGER.debug("(%s) Dates loaded, firing a garbage_collection_loaded event", self._name) + _LOGGER.debug( + "(%s) Dates loaded, firing a garbage_collection_loaded event", self._name + ) """ TO DO """ event_data = { "entity_id": self.entity_id, - "collection_dates": dates_to_texts(self._collection_dates) + "collection_dates": dates_to_texts(self._collection_dates), } self.hass.bus.async_fire("garbage_collection_loaded", event_data) # self.hass.bus.fire("garbage_collection_loaded", event_data) From 033e3aff1263ffceebdb21c8c8838d5ab98e1f78 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Mon, 3 Jan 2022 21:22:11 +0100 Subject: [PATCH 05/30] added manual update service --- .../garbage_collection/__init__.py | 21 +++++++++++++++- custom_components/garbage_collection/const.py | 8 +++++++ .../garbage_collection/manifest.json | 2 +- .../garbage_collection/sensor.py | 24 +++++++++---------- .../garbage_collection/services.yaml | 7 +++++- .../garbage_collection/translations/cs.json | 1 + .../garbage_collection/translations/en.json | 1 + .../garbage_collection/translations/es.json | 1 + .../garbage_collection/translations/et.json | 1 + .../garbage_collection/translations/fr.json | 1 + .../garbage_collection/translations/it.json | 1 + .../garbage_collection/translations/pl.json | 1 + 12 files changed, 54 insertions(+), 15 deletions(-) diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py index 0127f06..6b1c4fe 100644 --- a/custom_components/garbage_collection/__init__.py +++ b/custom_components/garbage_collection/__init__.py @@ -43,12 +43,28 @@ } ) +UPDATE_STATE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): cv.string, + } +) + async def async_setup(hass, config): """Set up this component using YAML.""" + def handle_update_state(call): + """Handle the update_state service call.""" + entity_id = call.data.get(CONF_ENTITY_ID) + _LOGGER.debug("called update_state for %s", entity_id) + try: + entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] + await entity.async_update_state() + except Exception as err: + _LOGGER.error("Failed updating state for %s - %s", entity_id, err) + def handle_collect_garbage(call): - """Handle the service call.""" + """Handle the collect_garbage service call.""" entity_id = call.data.get(CONF_ENTITY_ID) last_collection = call.data.get(ATTR_LAST_COLLECTION) _LOGGER.debug("called collect_garbage for %s", entity_id) @@ -66,6 +82,9 @@ def handle_collect_garbage(call): hass.services.async_register( DOMAIN, "collect_garbage", handle_collect_garbage, schema=COLLECT_NOW_SCHEMA ) + hass.services.async_register( + DOMAIN, "update_state", handle_update_state, schema=UPDATE_STATE_SCHEMA + ) else: _LOGGER.debug("Service already registered") diff --git a/custom_components/garbage_collection/const.py b/custom_components/garbage_collection/const.py index 28b89c5..279ebdf 100644 --- a/custom_components/garbage_collection/const.py +++ b/custom_components/garbage_collection/const.py @@ -31,6 +31,7 @@ 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" @@ -235,6 +236,13 @@ class configuration(config_singularity): "type": bool, "validator": cv.boolean, }, + CONF_MANUAL: { + "step": 1, + "method": vol.Optional, + "default": False, + "type": bool, + "validator": cv.boolean, + }, CONF_FREQUENCY: { "step": 1, "method": vol.Required, diff --git a/custom_components/garbage_collection/manifest.json b/custom_components/garbage_collection/manifest.json index ec9aeb0..e85bb6c 100644 --- a/custom_components/garbage_collection/manifest.json +++ b/custom_components/garbage_collection/manifest.json @@ -1,7 +1,7 @@ { "domain": "garbage_collection", "name": "Garbage Collection", - "version": "1.1", + "version": "3.21", "documentation": "https://github.com/bruxy70/Garbage-Collection/", "issue_tracker": "https://github.com/bruxy70/Garbage-Collection/issues", "iot_class": "calculated", diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index 57cc989..9874c6d 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -38,6 +38,7 @@ CONF_ICON_TOMORROW, CONF_INCLUDE_DATES, CONF_LAST_MONTH, + CONF_MANUAL, CONF_MOVE_COUNTRY_HOLIDAYS, CONF_OBSERVED, CONF_OFFSET, @@ -60,7 +61,7 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) +SCAN_INTERVAL = timedelta(seconds=10) THROTTLE_INTERVAL = timedelta(seconds=60) @@ -159,6 +160,7 @@ def __init__(self, hass, config, title=None): self._name = title if title is not None else config.get(CONF_NAME) self._hidden = config.get(ATTR_HIDDEN, False) self._frequency = config.get(CONF_FREQUENCY) + self._manual = config.get(CONF_MANUAL) self._collection_days = config.get(CONF_COLLECTION_DAYS) first_month = config.get(CONF_FIRST_MONTH) if first_month in MONTH_OPTIONS: @@ -707,29 +709,27 @@ async def async_next_date( async def async_update(self) -> None: """Get the latest data and updates the states.""" - if not await self._async_ready_for_update(): + if not await self._async_ready_for_update() or not self.hass.is_running: return + _LOGGER.debug("(%s) Calling update", self._name) - now = dt_util.now() - today = now.date() await self._async_load_collection_dates() _LOGGER.debug( "(%s) Dates loaded, firing a garbage_collection_loaded event", self._name ) - - """ - TO DO - """ event_data = { "entity_id": self.entity_id, "collection_dates": dates_to_texts(self._collection_dates), } self.hass.bus.async_fire("garbage_collection_loaded", event_data) - # self.hass.bus.fire("garbage_collection_loaded", event_data) + if not self._manual: + await self.async_update_state() - _LOGGER.debug( - "(%s) Event haldlers finished, looking for next collection", self._name - ) + async def async_update_state(self) -> None: + """Pick the first event from collection dates, update attrubutes.""" + _LOGGER.debug("(%s) Looking for next collection", self._name) + now = dt_util.now() + today = now.date() self._next_date = await self.async_next_date(today) self._last_updated = now if self._next_date is not None: diff --git a/custom_components/garbage_collection/services.yaml b/custom_components/garbage_collection/services.yaml index 29b6e67..e63327a 100644 --- a/custom_components/garbage_collection/services.yaml +++ b/custom_components/garbage_collection/services.yaml @@ -7,4 +7,9 @@ collect_garbage: last_collection: description: Date and time of the last collection (optional) example: "2020-08-16 10:54:00" - \ No newline at end of file +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. + 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 index e27c861..ac41c06 100644 --- a/custom_components/garbage_collection/translations/cs.json +++ b/custom_components/garbage_collection/translations/cs.json @@ -8,6 +8,7 @@ "name": "Friendly name", "hidden": "Skrýt v kalendáři", "frequency": "Frekvence", + "manual_update": "Sensor state aktualozvaný 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)", diff --git a/custom_components/garbage_collection/translations/en.json b/custom_components/garbage_collection/translations/en.json index d2f6a37..eaa9e17 100644 --- a/custom_components/garbage_collection/translations/en.json +++ b/custom_components/garbage_collection/translations/en.json @@ -7,6 +7,7 @@ "data": { "name": "Friendly name", "hidden": "Hide in calendar", + "manual_update": "Sensor state updated only by calling service", "frequency": "Frequency", "icon_normal": "Icon (mdi:trash-can) - optional", "icon_tomorrow": "Icon collection tomorrow (mdi:delete-restore) - optional", diff --git a/custom_components/garbage_collection/translations/es.json b/custom_components/garbage_collection/translations/es.json index 2313c67..d93d4ef 100644 --- a/custom_components/garbage_collection/translations/es.json +++ b/custom_components/garbage_collection/translations/es.json @@ -8,6 +8,7 @@ "name": "Nombre amigable", "hidden": "Esconderse en el calendario", "frequency": "Frequencia", + "manual_update": "Sensor state updated only by calling service", "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)", diff --git a/custom_components/garbage_collection/translations/et.json b/custom_components/garbage_collection/translations/et.json index 6316c55..2b25978 100644 --- a/custom_components/garbage_collection/translations/et.json +++ b/custom_components/garbage_collection/translations/et.json @@ -8,6 +8,7 @@ "name": "Kuvatav nimi", "hidden": "Ära näita kalendris", "frequency": "Sagedus", + "manual_update": "Sensor state updated only by calling 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", diff --git a/custom_components/garbage_collection/translations/fr.json b/custom_components/garbage_collection/translations/fr.json index 2182347..a04a19e 100644 --- a/custom_components/garbage_collection/translations/fr.json +++ b/custom_components/garbage_collection/translations/fr.json @@ -8,6 +8,7 @@ "name": "Friendly name", "hidden": "Masquer dans le calendrier", "frequency": "Fréquence", + "manual_update": "Sensor state updated only by calling 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)", diff --git a/custom_components/garbage_collection/translations/it.json b/custom_components/garbage_collection/translations/it.json index a01d232..3f86b7b 100644 --- a/custom_components/garbage_collection/translations/it.json +++ b/custom_components/garbage_collection/translations/it.json @@ -8,6 +8,7 @@ "name": "Nome personalizzato", "hidden": "Nascondi nel calendario", "frequency": "Frequenza", + "manual_update": "Sensor state updated only by calling service", "icon_normal": "Icona (mdi:trash-can)", "icon_tomorrow": "Icona raccolta domani (mdi:delete-restore)", "icon_today": "Icona raccolta oggi (mdi:delete-circle)", diff --git a/custom_components/garbage_collection/translations/pl.json b/custom_components/garbage_collection/translations/pl.json index ce7c748..541aa39 100644 --- a/custom_components/garbage_collection/translations/pl.json +++ b/custom_components/garbage_collection/translations/pl.json @@ -8,6 +8,7 @@ "name": "Przyjazna nazwa", "hidden": "Ukryj w kalendarzu", "frequency": "Częstotliwość", + "manual_update": "Sensor state updated only by calling service", "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", From d93df78c35dbfc398a4b5ed3d5cc2b3e62e9c80a Mon Sep 17 00:00:00 2001 From: Vaclav Date: Mon, 3 Jan 2022 22:52:43 +0100 Subject: [PATCH 06/30] pydocstyle --- custom_components/garbage_collection/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index 9874c6d..f5649f5 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -141,7 +141,7 @@ def to_dates(dates: List[Any]) -> List[date]: def dates_to_texts(dates: List[date]) -> List[str]: - """Convert list of dates to texts""" + """Convert list of dates to texts.""" converted = [] # type: List[str] for day in dates: try: From b8cf5e96a1b4ee9a3d782c12dcf5720801a53f9b Mon Sep 17 00:00:00 2001 From: Vaclav Date: Mon, 3 Jan 2022 23:17:41 +0100 Subject: [PATCH 07/30] style --- custom_components/garbage_collection/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py index 6b1c4fe..5049dd4 100644 --- a/custom_components/garbage_collection/__init__.py +++ b/custom_components/garbage_collection/__init__.py @@ -53,17 +53,17 @@ async def async_setup(hass, config): """Set up this component using YAML.""" - def handle_update_state(call): + async def handle_update_state(call): """Handle the update_state service call.""" entity_id = call.data.get(CONF_ENTITY_ID) _LOGGER.debug("called update_state for %s", entity_id) try: entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] - await entity.async_update_state() + entity.async_update_state() except Exception as err: _LOGGER.error("Failed updating state for %s - %s", entity_id, err) - def handle_collect_garbage(call): + async def handle_collect_garbage(call): """Handle the collect_garbage service call.""" entity_id = call.data.get(CONF_ENTITY_ID) last_collection = call.data.get(ATTR_LAST_COLLECTION) From 9eaf47d96a5591ad491ddd8e44a5f33fb60b2dcb Mon Sep 17 00:00:00 2001 From: Vaclav Date: Mon, 3 Jan 2022 23:18:43 +0100 Subject: [PATCH 08/30] await --- custom_components/garbage_collection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py index 5049dd4..aba181b 100644 --- a/custom_components/garbage_collection/__init__.py +++ b/custom_components/garbage_collection/__init__.py @@ -59,7 +59,7 @@ async def handle_update_state(call): _LOGGER.debug("called update_state for %s", entity_id) try: entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] - entity.async_update_state() + await entity.async_update_state() except Exception as err: _LOGGER.error("Failed updating state for %s - %s", entity_id, err) From c6961f4829c6dae6d9fe0982d5d5c840bb2c5334 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Tue, 4 Jan 2022 21:01:51 +0100 Subject: [PATCH 09/30] add and remove date services --- .../garbage_collection/__init__.py | 40 ++++++++++++++++++- .../garbage_collection/sensor.py | 26 +++++++----- .../garbage_collection/services.yaml | 18 +++++++++ .../garbage_collection/translations/cs.json | 2 +- .../garbage_collection/translations/en.json | 2 +- .../garbage_collection/translations/es.json | 2 +- .../garbage_collection/translations/et.json | 2 +- .../garbage_collection/translations/fr.json | 2 +- .../garbage_collection/translations/it.json | 2 +- .../garbage_collection/translations/pl.json | 2 +- 10 files changed, 80 insertions(+), 18 deletions(-) diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py index aba181b..e9606e9 100644 --- a/custom_components/garbage_collection/__init__.py +++ b/custom_components/garbage_collection/__init__.py @@ -12,6 +12,7 @@ from .const import ( ATTR_LAST_COLLECTION, + CONF_DATE, CONF_FREQUENCY, CONF_SENSORS, DOMAIN, @@ -49,10 +50,41 @@ } ) +ADD_REMOVE_DATE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): cv.string, + vol.Required(CONF_DATE): cv.date, + } +) + async def async_setup(hass, config): """Set up this component using YAML.""" + async def handle_add_date(call): + """Handle the add_date service call.""" + entity_id = call.data.get(CONF_ENTITY_ID) + dt = call.data.get(CONF_DATE) + _LOGGER.debug("called add_date %s to %s", dt, entity_id) + try: + entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] + _LOGGER.debug("date type is %s", type(dt)) + # await entity.add_date(dt) + except Exception as err: + _LOGGER.error("Failed adding date for %s - %s", entity_id, err) + + async def handle_remove_date(call): + """Handle the remove_date service call.""" + entity_id = call.data.get(CONF_ENTITY_ID) + dt = call.data.get(CONF_DATE) + _LOGGER.debug("called remove_date %s to %s", dt, entity_id) + try: + entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] + _LOGGER.debug("date type is %s", type(dt)) + # await entity.remove_date(dt) + except Exception as err: + _LOGGER.error("Failed removing date for %s - %s", entity_id, err) + async def handle_update_state(call): """Handle the update_state service call.""" entity_id = call.data.get(CONF_ENTITY_ID) @@ -85,8 +117,14 @@ async def handle_collect_garbage(call): hass.services.async_register( DOMAIN, "update_state", handle_update_state, schema=UPDATE_STATE_SCHEMA ) + hass.services.async_register( + DOMAIN, "add_date", handle_add_date, schema=ADD_REMOVE_DATE_SCHEMA + ) + hass.services.async_register( + DOMAIN, "remove_date", handle_remove_date, schema=ADD_REMOVE_DATE_SCHEMA + ) else: - _LOGGER.debug("Service already registered") + _LOGGER.debug("Services already registered") if config.get(DOMAIN) is None: # We get here if the integration is set up using config flow diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index f5649f5..2265f02 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -106,9 +106,9 @@ def to_date(day: Any) -> date: """Convert datetime or text to date, if not already datetime.""" if day is None: raise ValueError - if type(day) == date: + if isinstance(day, date): return day - if type(day) == datetime: + if isinstance(day, datetime): return day.date() return date.fromisoformat(day) @@ -240,7 +240,7 @@ async def async_load_holidays(self, today: date) -> None: kwargs["prov"] = self._holiday_prov if ( self._holiday_observed is not None - and type(self._holiday_observed) is bool + and isinstance(self._holiday_observed, bool) and not self._holiday_observed ): kwargs["observed"] = self._holiday_observed # type: ignore @@ -252,11 +252,11 @@ async def async_load_holidays(self, today: date) -> None: except Exception as err: _LOGGER.error("(%s) Holiday not removed (%s)", self._name, err) try: - for d, name in hol.items(): - self._holidays.append(d) - log += f"\n {d}: {name}" - if d >= today and d <= year_from_today: - self._holidays_log += f"\n {d}: {name}" + for holiday_date, holiday_name in hol.items(): + self._holidays.append(holiday_date) + log += f"\n {holiday_date}: {holiday_name}" + if holiday_date >= today and holiday_date <= year_from_today: + self._holidays_log += f"\n {holiday_date}: {holiday_name}" except KeyError: _LOGGER.error( "(%s) Invalid country code (%s)", @@ -589,11 +589,11 @@ async def _async_ready_for_update(self) -> bool: try: if self._next_date == today and ( ( - type(self.expire_after) is time + isinstance(self.expire_after, time) and now.time() >= self.expire_after ) or ( - type(self.last_collection) is datetime + isinstance(self.last_collection, datetime) and self.last_collection.date() == today ) ): @@ -684,6 +684,12 @@ async def _async_load_collection_dates(self) -> None: d = await self._async_find_next_date(d + timedelta(days=1)) self._collection_dates.sort() + async def add_date(self, collection_date: date) -> None: + """Add date to _collection_dates""" + + async def remove_date(self, collection_date: date) -> None: + """Remove date from _collection dates""" + async def async_next_date( self, first_date: date, ignore_today=False ) -> Optional[date]: diff --git a/custom_components/garbage_collection/services.yaml b/custom_components/garbage_collection/services.yaml index e63327a..f4b23b4 100644 --- a/custom_components/garbage_collection/services.yaml +++ b/custom_components/garbage_collection/services.yaml @@ -7,6 +7,24 @@ collect_garbage: 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. + fields: + entity_id: + description: The garbage_collection sensor entity_id + example: sensor.general_waste + date: + description: Collection date to add + example: "2020-08-16" +remove_date: + description: Remove automatically calculated collection date. + 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. fields: diff --git a/custom_components/garbage_collection/translations/cs.json b/custom_components/garbage_collection/translations/cs.json index ac41c06..0dfab12 100644 --- a/custom_components/garbage_collection/translations/cs.json +++ b/custom_components/garbage_collection/translations/cs.json @@ -8,7 +8,7 @@ "name": "Friendly name", "hidden": "Skrýt v kalendáři", "frequency": "Frekvence", - "manual_update": "Sensor state aktualozvaný voláním služby", + "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)", diff --git a/custom_components/garbage_collection/translations/en.json b/custom_components/garbage_collection/translations/en.json index eaa9e17..885252f 100644 --- a/custom_components/garbage_collection/translations/en.json +++ b/custom_components/garbage_collection/translations/en.json @@ -7,7 +7,7 @@ "data": { "name": "Friendly name", "hidden": "Hide in calendar", - "manual_update": "Sensor state updated only by calling service", + "manual_update": "Sensor state updated manually by calling a service", "frequency": "Frequency", "icon_normal": "Icon (mdi:trash-can) - optional", "icon_tomorrow": "Icon collection tomorrow (mdi:delete-restore) - optional", diff --git a/custom_components/garbage_collection/translations/es.json b/custom_components/garbage_collection/translations/es.json index d93d4ef..faeae96 100644 --- a/custom_components/garbage_collection/translations/es.json +++ b/custom_components/garbage_collection/translations/es.json @@ -8,7 +8,7 @@ "name": "Nombre amigable", "hidden": "Esconderse en el calendario", "frequency": "Frequencia", - "manual_update": "Sensor state updated only by calling service", + "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)", diff --git a/custom_components/garbage_collection/translations/et.json b/custom_components/garbage_collection/translations/et.json index 2b25978..056c9e1 100644 --- a/custom_components/garbage_collection/translations/et.json +++ b/custom_components/garbage_collection/translations/et.json @@ -8,7 +8,7 @@ "name": "Kuvatav nimi", "hidden": "Ära näita kalendris", "frequency": "Sagedus", - "manual_update": "Sensor state updated only by calling service", + "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", diff --git a/custom_components/garbage_collection/translations/fr.json b/custom_components/garbage_collection/translations/fr.json index a04a19e..03c4548 100644 --- a/custom_components/garbage_collection/translations/fr.json +++ b/custom_components/garbage_collection/translations/fr.json @@ -8,7 +8,7 @@ "name": "Friendly name", "hidden": "Masquer dans le calendrier", "frequency": "Fréquence", - "manual_update": "Sensor state updated only by calling service", + "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)", diff --git a/custom_components/garbage_collection/translations/it.json b/custom_components/garbage_collection/translations/it.json index 3f86b7b..d4492ad 100644 --- a/custom_components/garbage_collection/translations/it.json +++ b/custom_components/garbage_collection/translations/it.json @@ -8,7 +8,7 @@ "name": "Nome personalizzato", "hidden": "Nascondi nel calendario", "frequency": "Frequenza", - "manual_update": "Sensor state updated only by calling service", + "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)", diff --git a/custom_components/garbage_collection/translations/pl.json b/custom_components/garbage_collection/translations/pl.json index 541aa39..ea6c2ad 100644 --- a/custom_components/garbage_collection/translations/pl.json +++ b/custom_components/garbage_collection/translations/pl.json @@ -8,7 +8,7 @@ "name": "Przyjazna nazwa", "hidden": "Ukryj w kalendarzu", "frequency": "Częstotliwość", - "manual_update": "Sensor state updated only by calling service", + "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", From 6bd815f79aa4d85303968295d7be1f765c48a949 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Tue, 4 Jan 2022 21:22:14 +0100 Subject: [PATCH 10/30] completed add and remove serices --- custom_components/garbage_collection/__init__.py | 14 ++++++-------- custom_components/garbage_collection/sensor.py | 6 ++++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py index e9606e9..3aa6243 100644 --- a/custom_components/garbage_collection/__init__.py +++ b/custom_components/garbage_collection/__init__.py @@ -64,24 +64,22 @@ async def async_setup(hass, config): async def handle_add_date(call): """Handle the add_date service call.""" entity_id = call.data.get(CONF_ENTITY_ID) - dt = call.data.get(CONF_DATE) - _LOGGER.debug("called add_date %s to %s", dt, entity_id) + collection_date = call.data.get(CONF_DATE) + _LOGGER.debug("called add_date %s to %s", collection_date, entity_id) try: entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] - _LOGGER.debug("date type is %s", type(dt)) - # await entity.add_date(dt) + await entity.add_date(collection_date) except Exception as err: _LOGGER.error("Failed adding date for %s - %s", entity_id, err) async def handle_remove_date(call): """Handle the remove_date service call.""" entity_id = call.data.get(CONF_ENTITY_ID) - dt = call.data.get(CONF_DATE) - _LOGGER.debug("called remove_date %s to %s", dt, entity_id) + collection_date = call.data.get(CONF_DATE) + _LOGGER.debug("called remove_date %s to %s", collection_date, entity_id) try: entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] - _LOGGER.debug("date type is %s", type(dt)) - # await entity.remove_date(dt) + await entity.remove_date(collection_date) except Exception as err: _LOGGER.error("Failed removing date for %s - %s", entity_id, err) diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index 2265f02..03b6330 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -686,9 +686,15 @@ async def _async_load_collection_dates(self) -> None: 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: + raise KeyError(f"{collection_date} already on the collection schedule") async def remove_date(self, collection_date: date) -> None: """Remove date from _collection dates""" + self._collection_dates.remove(collection_date) async def async_next_date( self, first_date: date, ignore_today=False From f369c6c1ef15d8cfae56afa86bd211584d0a6a5e Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Wed, 5 Jan 2022 09:56:58 +0100 Subject: [PATCH 11/30] Pydocstyle --- custom_components/garbage_collection/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index 03b6330..22f8962 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -685,7 +685,7 @@ async def _async_load_collection_dates(self) -> None: self._collection_dates.sort() async def add_date(self, collection_date: date) -> None: - """Add date to _collection_dates""" + """Add date to _collection_dates.""" if collection_date not in self._collection_dates: self._collection_dates.append(collection_date) self._collection_dates.sort() @@ -693,7 +693,7 @@ async def add_date(self, collection_date: date) -> None: raise KeyError(f"{collection_date} already on the collection schedule") async def remove_date(self, collection_date: date) -> None: - """Remove date from _collection dates""" + """Remove date from _collection dates.""" self._collection_dates.remove(collection_date) async def async_next_date( From cea841449503e341e72cfda67c47548e8eb02826 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Wed, 5 Jan 2022 19:00:33 +0100 Subject: [PATCH 12/30] fixed update state --- custom_components/garbage_collection/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py index 3aa6243..064aca0 100644 --- a/custom_components/garbage_collection/__init__.py +++ b/custom_components/garbage_collection/__init__.py @@ -104,9 +104,9 @@ async def handle_collect_garbage(call): entity.last_collection = dt_util.now() else: entity.last_collection = dt_util.as_local(last_collection) + await entity.async_update_state() except Exception as err: _LOGGER.error("Failed setting last collection for %s - %s", entity_id, err) - hass.services.call("homeassistant", "update_entity", {"entity_id": entity_id}) if DOMAIN not in hass.services.async_services(): hass.services.async_register( From 5bc760ddabb57a84dde2204251f6728e8a469056 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Wed, 5 Jan 2022 20:09:22 +0100 Subject: [PATCH 13/30] services and tanslations --- custom_components/garbage_collection/services.yaml | 12 ++++++++++++ .../garbage_collection/translations/cs.json | 1 + .../garbage_collection/translations/en.json | 3 ++- .../garbage_collection/translations/es.json | 1 + .../garbage_collection/translations/et.json | 1 + .../garbage_collection/translations/fr.json | 3 ++- .../garbage_collection/translations/it.json | 1 + .../garbage_collection/translations/pl.json | 1 + 8 files changed, 21 insertions(+), 2 deletions(-) diff --git a/custom_components/garbage_collection/services.yaml b/custom_components/garbage_collection/services.yaml index f4b23b4..2002fec 100644 --- a/custom_components/garbage_collection/services.yaml +++ b/custom_components/garbage_collection/services.yaml @@ -1,5 +1,8 @@ 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 @@ -9,6 +12,9 @@ collect_garbage: 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 @@ -18,6 +24,9 @@ add_date: example: "2020-08-16" remove_date: description: Remove automatically calculated collection date. + target: + entity: + integration: garbage_collection fields: entity_id: description: The garbage_collection sensor entity_id @@ -27,6 +36,9 @@ remove_date: 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 diff --git a/custom_components/garbage_collection/translations/cs.json b/custom_components/garbage_collection/translations/cs.json index 0dfab12..cc9c15a 100644 --- a/custom_components/garbage_collection/translations/cs.json +++ b/custom_components/garbage_collection/translations/cs.json @@ -98,6 +98,7 @@ "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)", diff --git a/custom_components/garbage_collection/translations/en.json b/custom_components/garbage_collection/translations/en.json index 885252f..eaa199f 100644 --- a/custom_components/garbage_collection/translations/en.json +++ b/custom_components/garbage_collection/translations/en.json @@ -7,8 +7,8 @@ "data": { "name": "Friendly name", "hidden": "Hide in calendar", - "manual_update": "Sensor state updated manually by calling a service", "frequency": "Frequency", + "manual_update": "Sensor state updated manually by calling a service", "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", @@ -97,6 +97,7 @@ "data": { "hidden": "Hide in calendar", "frequency": "Frequency", + "manual_update": "Sensor state updated manually by calling a service", "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", diff --git a/custom_components/garbage_collection/translations/es.json b/custom_components/garbage_collection/translations/es.json index faeae96..0aa9e51 100644 --- a/custom_components/garbage_collection/translations/es.json +++ b/custom_components/garbage_collection/translations/es.json @@ -97,6 +97,7 @@ "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)", diff --git a/custom_components/garbage_collection/translations/et.json b/custom_components/garbage_collection/translations/et.json index 056c9e1..080445f 100644 --- a/custom_components/garbage_collection/translations/et.json +++ b/custom_components/garbage_collection/translations/et.json @@ -98,6 +98,7 @@ "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", diff --git a/custom_components/garbage_collection/translations/fr.json b/custom_components/garbage_collection/translations/fr.json index 03c4548..302f9e3 100644 --- a/custom_components/garbage_collection/translations/fr.json +++ b/custom_components/garbage_collection/translations/fr.json @@ -92,8 +92,9 @@ "title": "Garbage Collection - Fréquence de la collecte", "description": "Modifier les paramètres des capteurs. Plus d'infos sur https://github.com/bruxy70/Garbage-Collection", "data": { - "frequency": "Fréquence", "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)", diff --git a/custom_components/garbage_collection/translations/it.json b/custom_components/garbage_collection/translations/it.json index d4492ad..7268889 100644 --- a/custom_components/garbage_collection/translations/it.json +++ b/custom_components/garbage_collection/translations/it.json @@ -94,6 +94,7 @@ "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)", diff --git a/custom_components/garbage_collection/translations/pl.json b/custom_components/garbage_collection/translations/pl.json index ea6c2ad..c710d16 100644 --- a/custom_components/garbage_collection/translations/pl.json +++ b/custom_components/garbage_collection/translations/pl.json @@ -97,6 +97,7 @@ "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", From bf825d7e8a597ed5bd347dd2c3f73ea8cde2eced Mon Sep 17 00:00:00 2001 From: Vaclav Date: Wed, 5 Jan 2022 20:38:08 +0100 Subject: [PATCH 14/30] remove target --- custom_components/garbage_collection/services.yaml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/custom_components/garbage_collection/services.yaml b/custom_components/garbage_collection/services.yaml index 2002fec..f4b23b4 100644 --- a/custom_components/garbage_collection/services.yaml +++ b/custom_components/garbage_collection/services.yaml @@ -1,8 +1,5 @@ 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 @@ -12,9 +9,6 @@ collect_garbage: 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 @@ -24,9 +18,6 @@ add_date: example: "2020-08-16" remove_date: description: Remove automatically calculated collection date. - target: - entity: - integration: garbage_collection fields: entity_id: description: The garbage_collection sensor entity_id @@ -36,9 +27,6 @@ remove_date: 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 From 3a6e9f71f80eff2c8ca89f55cadc3a85944c3121 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Wed, 5 Jan 2022 22:22:42 +0100 Subject: [PATCH 15/30] readme --- README.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a5be3df..514fa4b 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ Entity_id change is not possible using the YAML configuration. Changing other pa |:----------|----------|------------ | `name` | Yes | Sensor friendly name | `frequency` | Yes | `"weekly"`, `"even-weeks"`, `"odd-weeks"`, `"every-n-weeks"`, `"every-n-days"`, `"monthly"`, `"annual"` or `"group"` +| `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-example).
**Default**: `False` | `offset` | No | Offset calculated date by `offset` days (makes most sense for monthly frequency). Examples of use:
for last Saurday each month, configure first Saturday each month with `offset: -7`
for 1st Wednesday in of full week, configure first Monday each month with `offset: 2`
(integer between -31 and 31) **Default**: 0 | `hidden` | No | Hide in calendar (useful for sensors that are used in groups)
**Default**: `False` | `icon_normal` | No | Default icon **Default**: `mdi:trash-can` @@ -215,7 +216,90 @@ It will set the `last_collection` attribute to the current date and time. | Attribute | Description |:----------|------------ -| `entity_id` | The gatbage collection entity id (e.g. `sensor.general_waste`) +| `entity_id` | The garbage collection entity id (e.g. `sensor.general_waste`) + + +### garbage_collection.add_date +...see the manual update example below + +### garbage_collection.remove_date +...see the manual update example below + +### garbage_collection.update_state +...see the manual update example below + + +## Manual update example +(!!! Advanced - if you think this is too complicated, then this is not for you !!!) + +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 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 servics `garbage_collection.add_date` and/or `garbage_collection.remove_date` to programatically tweak the dates in whatever way you need (e.g. based on values from external API sensor, comparing the dates with list of holidays, calculating custom offsets based on the day of the week etc.). This is complicated, but gives you an ultimate flexibility. + +### 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 next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with the dates added and removed 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`) + +### 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 next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with the dates added and removed 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`) + +### 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. + +## Simple example +Adding an extra collection date (a fixed date in this case). + + +```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: sensor.test + date: '2022-01-07' + - service: garbage_collection.update_state + data: + entity_id: sensor.test +mode: single +``` + +## Advanced example +Checking the automatically created collection schedule, comparing with list of public holidays and moving schecule by a custom offset. + +TBD... + # Lovelace config examples From b82454a994100a1788c4030a3871fdfb4c210f36 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Wed, 5 Jan 2022 22:30:50 +0100 Subject: [PATCH 16/30] update lovelace template --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 514fa4b..281e1b4 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,7 @@ This is the configuration card: type: picture-entity name_template: >- - {{ states.sensor.bio.attributes.days }} days + {{ state_attr('sensor.bio','days') }} days show_name: True show_state: False entity: sensor.bio From 62f1892193423edf8ed098411a6d75af55b1ad92 Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Wed, 5 Jan 2022 23:00:05 +0100 Subject: [PATCH 17/30] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 281e1b4..5ebc538 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ You will **have to create an automation triggered by this event**. In this autom ### 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 next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with the dates added and removed by the automation. +Note that this date will be removed on the next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with the dates added and removed by the automation. | Attribute | Description |:----------|------------ @@ -248,7 +248,7 @@ Note that this date will be removed on next sensor update when data is re-calcul ### 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 next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with the dates added and removed by the automation. +Note that this date will reappear on the next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with the dates added and removed by the automation. | Attribute | Description |:----------|------------ From ecdad8940204075cfc90ffc6527b6eee3638d89e Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Wed, 5 Jan 2022 23:44:22 +0100 Subject: [PATCH 18/30] Update info.md --- info.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/info.md b/info.md index 3e2cdce..a86f712 100644 --- a/info.md +++ b/info.md @@ -1,4 +1,4 @@ -[![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/2021.svg) +[![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) @@ -77,3 +77,5 @@ It will set the `last_collection` attribute to the current date and time. | Attribute | Description |:----------|------------ | `entity_id` | The gatbage collection entity id (e.g. `sensor.general_waste`) + +For more details see the repository. From 7be38ba80e5ed526653ff47e2f2b0dd60451c760 Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Thu, 6 Jan 2022 21:24:38 +0100 Subject: [PATCH 19/30] Update README.md --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 5ebc538..0543452 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,32 @@ action: mode: single ``` +## Moderate example +This will loop through the calculated dates, and add extra collection to a day after each calculated one. So if this is set for a collection each first Wednesday each month, it will result in a collection on first Wednesday, and the following day (kind of first Thursday, except if the week is starting on Thursday - just a random weird example :). + +```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: + count: '{{ trigger.event.data.collection_dates | count }}' + sequence: + - service: garbage_collection.add_date + data: + entity_id: sensor.test + date: >- + {{( as_datetime(trigger.event.data.collection_dates[repeat.index]) + timedelta( days = 1)) | as_timestamp | timestamp_custom("%Y-%m-%d") }} + - service: garbage_collection.update_state + data: + entity_id: sensor.test +mode: single +``` + ## Advanced example Checking the automatically created collection schedule, comparing with list of public holidays and moving schecule by a custom offset. From 1e6eabfa30a9c6ce32dc13e471e4e5edf12530db Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Thu, 6 Jan 2022 22:26:36 +0100 Subject: [PATCH 20/30] Update README.md --- README.md | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0543452..8131619 100644 --- a/README.md +++ b/README.md @@ -322,10 +322,47 @@ mode: single ``` ## Advanced example -Checking the automatically created collection schedule, comparing with list of public holidays and moving schecule by a custom offset. - -TBD... +This is an equivalent of "holiday in week" move - checking if there is a public holiday on the calculated collection day, or in the same day before, and if yes, moving the collection by one day. This is fully custom logic, so it toucl be further complicated by whatever rules anyone wants. +```yaml +alias: test garbage_collection event +description: >- + Loop through all calculated dates, move the collection by 1 day if 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 +condition: "{{ trigger.event.data.entity_id == 'sensor.test' }}" +action: + - repeat: + count: '{{ trigger.event.data.collection_dates | count }}' + sequence: + - condition: template + value_template: >- + {%- set ns = namespace(found=false) %} + {%- set d = trigger.event.data.collection_dates[repeat.index] %} + {%- for i in range(as_datetime(d).weekday()+1) %} + {%- if (as_datetime(d) + timedelta( days = -i)) | as_timestamp | timestamp_custom("%Y-%m-%d") in state_attr(trigger.event.data.entity_id,'holidays') %} + {%- set ns.found = true %} + {%- endif %} + {%- endfor %} + {{ ns.found }} + - service: garbage_collection.add_date + data: + entity_id: "{{ trigger.event.data.entity_id }}" + date: >- + {{ (as_datetime(trigger.event.data.collection_dates[repeat.index]) + timedelta( days = 1)) | as_timestamp | timestamp_custom("%Y-%m-%d") }} + - service: garbage_collection.remove_date + data: + entity_id: "{{ trigger.event.data.entity_id }}" + date: '{{ trigger.event.data.collection_dates[repeat.index] }}' + - service: garbage_collection.update_state + data: + entity_id: sensor.test +mode: single +``` # Lovelace config examples From e1598046a862b70e1dfd39f74daf79ff4c24e71e Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Thu, 6 Jan 2022 22:27:17 +0100 Subject: [PATCH 21/30] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 8131619..c42d562 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,6 @@ trigger: event_type: garbage_collection_loaded event_data: entity_id: sensor.test -condition: "{{ trigger.event.data.entity_id == 'sensor.test' }}" action: - repeat: count: '{{ trigger.event.data.collection_dates | count }}' From 77e50dde0b95cde54b4f7f197aaa8c04a8596623 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Thu, 6 Jan 2022 22:51:48 +0100 Subject: [PATCH 22/30] show all holidays --- custom_components/garbage_collection/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index 22f8962..42b4f38 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -255,8 +255,7 @@ async def async_load_holidays(self, today: date) -> None: for holiday_date, holiday_name in hol.items(): self._holidays.append(holiday_date) log += f"\n {holiday_date}: {holiday_name}" - if holiday_date >= today and holiday_date <= year_from_today: - self._holidays_log += f"\n {holiday_date}: {holiday_name}" + self._holidays_log += f"\n {holiday_date}: {holiday_name}" except KeyError: _LOGGER.error( "(%s) Invalid country code (%s)", From a7b8a3e6e0a02aa92746d4e40b2d98a1de68ff14 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Thu, 6 Jan 2022 23:07:05 +0100 Subject: [PATCH 23/30] readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c42d562..8ab8c1c 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,8 @@ Entity_id change is not possible using the YAML configuration. Changing other pa |:----------|----------|------------ | `first_month` | No | Month three letter abbreviation, e.g. `"jan"`, `"feb"`...
**Default**: `"jan"` | `last_month` | No | Month three letter abbreviation.
**Default**: `"dec"` -| `exclude_dates` | No | List of dates with no collection (using international date format `'yyyy-mm-dd'`. -| `include_dates` | No | List of extra collection (using international date format `'yyyy-mm-dd'`. +| `exclude_dates` | No | List of dates with no collection (using international date format `'yyyy-mm-dd'`. Make sure to enter the date in quotes! +| `include_dates` | No | List of extra collection (using international date format `'yyyy-mm-dd'`. Make sure to enter the date in quotes! | `move_country_holidays` | No | Country holidays - the country code (see [holidays](https://github.com/dr-prodigy/python-holidays) for the list of valid country codes).
Automatically move garbage collection on public holidays to the following day.
*Example:* `US` | `holiday_in_week_move` | No | Move garbage collection to the following day if a holiday is in week.
**Default**: `false` | `holiday_move_offset` | No | Move the collection by the number of days (integer -7..7) **Default**: 1 @@ -244,7 +244,7 @@ Note that this date will be removed on the next sensor update when data is re-ca | 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`) +| `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. @@ -253,7 +253,7 @@ Note that this date will reappear on the next sensor update when data is re-calc | 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`) +| `date` | The date to be removed, in ISO format (`'yyyy-mm-dd'`). Make sure to enter the date in quotes! ### 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. From bacc143d75535d748aeead3a4f49c35ae43cb440 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Thu, 6 Jan 2022 23:10:14 +0100 Subject: [PATCH 24/30] remove unused variable --- custom_components/garbage_collection/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/garbage_collection/sensor.py b/custom_components/garbage_collection/sensor.py index 42b4f38..bd302fd 100644 --- a/custom_components/garbage_collection/sensor.py +++ b/custom_components/garbage_collection/sensor.py @@ -219,7 +219,6 @@ async def async_load_holidays(self, today: date) -> None: """Load the holidays from from a date.""" self._holidays_log = "" log = "" - year_from_today = today + relativedelta(years=1) self._holidays.clear() if self._country_holidays is not None and self._country_holidays != "": this_year = today.year From 67363f32d05b824f79f199d52c0e7942952663af Mon Sep 17 00:00:00 2001 From: Vaclav Date: Thu, 6 Jan 2022 23:32:11 +0100 Subject: [PATCH 25/30] add target --- .../garbage_collection/__init__.py | 78 ++++++++++--------- .../garbage_collection/services.yaml | 16 +++- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py index 064aca0..0e4a165 100644 --- a/custom_components/garbage_collection/__init__.py +++ b/custom_components/garbage_collection/__init__.py @@ -39,20 +39,20 @@ COLLECT_NOW_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY_ID): cv.string, + vol.Required(CONF_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_LAST_COLLECTION): cv.datetime, } ) UPDATE_STATE_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY_ID): cv.string, + vol.Required(CONF_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]), } ) ADD_REMOVE_DATE_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY_ID): cv.string, + vol.Required(CONF_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]), vol.Required(CONF_DATE): cv.date, } ) @@ -63,50 +63,52 @@ async def async_setup(hass, config): async def handle_add_date(call): """Handle the add_date service call.""" - entity_id = call.data.get(CONF_ENTITY_ID) - collection_date = call.data.get(CONF_DATE) - _LOGGER.debug("called add_date %s to %s", collection_date, entity_id) - try: - entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] - await entity.add_date(collection_date) - except Exception as err: - _LOGGER.error("Failed adding date for %s - %s", entity_id, err) + for entity_id in call.data.get(CONF_ENTITY_ID): + collection_date = call.data.get(CONF_DATE) + _LOGGER.debug("called add_date %s to %s", collection_date, entity_id) + try: + entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] + await entity.add_date(collection_date) + except Exception as err: + _LOGGER.error("Failed adding date for %s - %s", entity_id, err) async def handle_remove_date(call): """Handle the remove_date service call.""" - entity_id = call.data.get(CONF_ENTITY_ID) - collection_date = call.data.get(CONF_DATE) - _LOGGER.debug("called remove_date %s to %s", collection_date, entity_id) - try: - entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] - await entity.remove_date(collection_date) - except Exception as err: - _LOGGER.error("Failed removing date for %s - %s", entity_id, err) + for entity_id in call.data.get(CONF_ENTITY_ID): + collection_date = call.data.get(CONF_DATE) + _LOGGER.debug("called remove_date %s to %s", collection_date, entity_id) + try: + entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] + await entity.remove_date(collection_date) + except Exception as err: + _LOGGER.error("Failed removing date for %s - %s", entity_id, err) async def handle_update_state(call): """Handle the update_state service call.""" - entity_id = call.data.get(CONF_ENTITY_ID) - _LOGGER.debug("called update_state for %s", entity_id) - try: - entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] - await entity.async_update_state() - except Exception as err: - _LOGGER.error("Failed updating state for %s - %s", entity_id, err) + for entity_id in call.data.get(CONF_ENTITY_ID): + _LOGGER.debug("called update_state for %s", entity_id) + try: + entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] + await entity.async_update_state() + except Exception as err: + _LOGGER.error("Failed updating state for %s - %s", entity_id, err) async def handle_collect_garbage(call): """Handle the collect_garbage service call.""" - entity_id = call.data.get(CONF_ENTITY_ID) - last_collection = call.data.get(ATTR_LAST_COLLECTION) - _LOGGER.debug("called collect_garbage for %s", entity_id) - try: - entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] - if last_collection is None: - entity.last_collection = dt_util.now() - else: - entity.last_collection = dt_util.as_local(last_collection) - await entity.async_update_state() - except Exception as err: - _LOGGER.error("Failed setting last collection for %s - %s", entity_id, err) + for entity_id in call.data.get(CONF_ENTITY_ID): + last_collection = call.data.get(ATTR_LAST_COLLECTION) + _LOGGER.debug("called collect_garbage for %s", entity_id) + try: + entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] + if last_collection is None: + entity.last_collection = dt_util.now() + else: + entity.last_collection = dt_util.as_local(last_collection) + await entity.async_update_state() + except Exception as err: + _LOGGER.error( + "Failed setting last collection for %s - %s", entity_id, err + ) if DOMAIN not in hass.services.async_services(): hass.services.async_register( diff --git a/custom_components/garbage_collection/services.yaml b/custom_components/garbage_collection/services.yaml index f4b23b4..ac31f0a 100644 --- a/custom_components/garbage_collection/services.yaml +++ b/custom_components/garbage_collection/services.yaml @@ -1,5 +1,8 @@ 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 @@ -9,24 +12,33 @@ collect_garbage: 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" + example: '"2020-08-16"' 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" + 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 From a99736ba04fa4280c34b2ace03d4172fdd6156ff Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Fri, 7 Jan 2022 09:35:11 +0100 Subject: [PATCH 26/30] Update README.md --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8ab8c1c..8dceafb 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ Event data: | `collection_dates` | List of collection dates calculated automatically. ## Simple example -Adding an extra collection date (a fixed date in this case). +Adding an extra collection date (a fixed date in this case) - for the entity `sensor.test`. ```yaml @@ -287,7 +287,7 @@ trigger: action: - service: garbage_collection.add_date data: - entity_id: sensor.test + entity_id: "{{ trigger.event.data.entity_id }}" date: '2022-01-07' - service: garbage_collection.update_state data: @@ -298,6 +298,8 @@ mode: single ## Moderate example This will loop through the calculated dates, and add extra collection to a day after each calculated one. So if this is set for a collection each first Wednesday each month, it will result in a collection on 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' @@ -312,23 +314,25 @@ action: sequence: - service: garbage_collection.add_date data: - entity_id: sensor.test + entity_id: "{{ trigger.event.data.entity_id }}" date: >- {{( as_datetime(trigger.event.data.collection_dates[repeat.index]) + timedelta( days = 1)) | as_timestamp | timestamp_custom("%Y-%m-%d") }} - service: garbage_collection.update_state data: - entity_id: sensor.test + 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 in the same day before, and if yes, moving the collection by one day. This is fully custom logic, so it toucl be further complicated by whatever rules anyone wants. +This is an equivalent of "holiday in week" move - checking if there is a public holiday on the calculated collection day, or in the same day before, 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. +Note that this does not disable the current holiday exception handling in the integration - so if you have also configured that to move the collection, it will move it one more day. So if you want to use, configure the `offset` to `0` (so that the integration movves the holiday by "zero" days). Or you can configure the calendar on another entity - the automation is really using them to check the list of the dates - the list could be anywhere, does not even have to be a garbage_collection entity. + +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, move the collection by 1 day if public holiday was in the week before or on the calculated collection date - calculate one + Loop through all calculated dates, move the collection by 1 day if public holiday was in the week before or on the calculated collection date calculate one trigger: - platform: event event_type: garbage_collection_loaded @@ -359,7 +363,7 @@ action: date: '{{ trigger.event.data.collection_dates[repeat.index] }}' - service: garbage_collection.update_state data: - entity_id: sensor.test + entity_id: "{{ trigger.event.data.entity_id }}" mode: single ``` From 3ff7678e09da02631f4de9eb22db3af9be483eb7 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Fri, 7 Jan 2022 11:45:19 +0100 Subject: [PATCH 27/30] offset --- README.md | 32 ++++++++++----- .../garbage_collection/__init__.py | 40 ++++++++++++++++++- .../garbage_collection/services.yaml | 15 +++++++ 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8ab8c1c..dd28c4c 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,9 @@ It will set the `last_collection` attribute to the current date and time. ### garbage_collection.remove_date ...see the manual update example below +### garbage_collection.offset_date +...see the manual update example below + ### garbage_collection.update_state ...see the manual update example below @@ -239,7 +242,7 @@ You will **have to create an automation triggered by this event**. In this autom ### 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 data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with the dates added and removed by the automation. +Note that this date will be removed on the next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with the dates added, removed or offset by the automation. | Attribute | Description |:----------|------------ @@ -248,12 +251,22 @@ Note that this date will be removed on the next sensor update when data is re-ca ### 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 reappear on the next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with the dates added and removed by the automation. +Note that this date will reappear on the next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with 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 offset will revert back on the next sensor update when data is re-calculated and loaded. This is why this service should be called from the automation triggered be the event `garbage_collection_loaded`, that 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 with 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. @@ -340,23 +353,20 @@ action: sequence: - condition: template value_template: >- + {%- set collection_date = as_datetime(trigger.event.data.collection_dates[repeat.index]) %} {%- set ns = namespace(found=false) %} - {%- set d = trigger.event.data.collection_dates[repeat.index] %} - {%- for i in range(as_datetime(d).weekday()+1) %} - {%- if (as_datetime(d) + timedelta( days = -i)) | as_timestamp | timestamp_custom("%Y-%m-%d") in state_attr(trigger.event.data.entity_id,'holidays') %} + {%- 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.add_date - data: - entity_id: "{{ trigger.event.data.entity_id }}" - date: >- - {{ (as_datetime(trigger.event.data.collection_dates[repeat.index]) + timedelta( days = 1)) | as_timestamp | timestamp_custom("%Y-%m-%d") }} - - service: garbage_collection.remove_date + - service: garbage_collection.offset_date data: entity_id: "{{ trigger.event.data.entity_id }}" date: '{{ trigger.event.data.collection_dates[repeat.index] }}' + offset: 1 - service: garbage_collection.update_state data: entity_id: sensor.test diff --git a/custom_components/garbage_collection/__init__.py b/custom_components/garbage_collection/__init__.py index 0e4a165..0fea609 100644 --- a/custom_components/garbage_collection/__init__.py +++ b/custom_components/garbage_collection/__init__.py @@ -6,6 +6,7 @@ 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 import config_entries from homeassistant.const import CONF_ENTITY_ID, CONF_NAME from homeassistant.helpers import discovery @@ -14,6 +15,7 @@ ATTR_LAST_COLLECTION, CONF_DATE, CONF_FREQUENCY, + CONF_OFFSET, CONF_SENSORS, DOMAIN, SENSOR_PLATFORM, @@ -57,6 +59,14 @@ } ) +OFFSET_DATE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_DATE): cv.date, + vol.Required(CONF_OFFSET): vol.All(vol.Coerce(int), vol.Range(min=-31, max=31)), + } +) + async def async_setup(hass, config): """Set up this component using YAML.""" @@ -65,7 +75,7 @@ async def handle_add_date(call): """Handle the add_date service call.""" for entity_id in call.data.get(CONF_ENTITY_ID): collection_date = call.data.get(CONF_DATE) - _LOGGER.debug("called add_date %s to %s", collection_date, entity_id) + _LOGGER.debug("called add_date %s from %s", collection_date, entity_id) try: entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] await entity.add_date(collection_date) @@ -76,13 +86,36 @@ async def handle_remove_date(call): """Handle the remove_date service call.""" for entity_id in call.data.get(CONF_ENTITY_ID): collection_date = call.data.get(CONF_DATE) - _LOGGER.debug("called remove_date %s to %s", collection_date, entity_id) + _LOGGER.debug("called remove_date %s from %s", collection_date, entity_id) try: entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] await entity.remove_date(collection_date) except Exception as err: _LOGGER.error("Failed removing date for %s - %s", entity_id, err) + async def handle_offset_date(call): + """Handle the offset_date service call.""" + for entity_id in call.data.get(CONF_ENTITY_ID): + offset = call.data.get(CONF_OFFSET) + collection_date = call.data.get(CONF_DATE) + _LOGGER.debug( + "called offset_date %s by %d days for %s", + collection_date, + offset, + entity_id, + ) + try: + new_date = collection_date + relativedelta(days=offset) + except Exception as err: + _LOGGER.error("Failed to offset the date - %s", err) + break + try: + entity = hass.data[DOMAIN][SENSOR_PLATFORM][entity_id] + await entity.remove_date(collection_date) + await entity.add_date(new_date) + except Exception as err: + _LOGGER.error("Failed ofsetting date for %s - %s", entity_id, err) + async def handle_update_state(call): """Handle the update_state service call.""" for entity_id in call.data.get(CONF_ENTITY_ID): @@ -123,6 +156,9 @@ async def handle_collect_garbage(call): hass.services.async_register( DOMAIN, "remove_date", handle_remove_date, schema=ADD_REMOVE_DATE_SCHEMA ) + hass.services.async_register( + DOMAIN, "offset_date", handle_offset_date, schema=OFFSET_DATE_SCHEMA + ) else: _LOGGER.debug("Services already registered") diff --git a/custom_components/garbage_collection/services.yaml b/custom_components/garbage_collection/services.yaml index ac31f0a..0eeed9a 100644 --- a/custom_components/garbage_collection/services.yaml +++ b/custom_components/garbage_collection/services.yaml @@ -22,6 +22,21 @@ add_date: 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: From b41fcd9db6c9f72be3068d2760d1da2abad5cc44 Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Fri, 7 Jan 2022 12:45:33 +0100 Subject: [PATCH 28/30] Update info.md --- info.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/info.md b/info.md index a86f712..a848400 100644 --- a/info.md +++ b/info.md @@ -46,7 +46,7 @@ garbage_collection: frequency: 'annual' date: '11/24' ``` -For more examples and configuration documentation check the repository file +For more examples and configuration documentation check the repository file ## STATE AND ATTRIBUTES @@ -76,6 +76,6 @@ It will set the `last_collection` attribute to the current date and time. | Attribute | Description |:----------|------------ -| `entity_id` | The gatbage collection entity id (e.g. `sensor.general_waste`) +| `entity_id` | The garbage collection entity id (e.g. `sensor.general_waste`) -For more details see the repository. +For more details see the repository. From 630490d5c7f855cf3cc20c09d33f1df4cc884d51 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Sat, 8 Jan 2022 19:02:39 +0100 Subject: [PATCH 29/30] #311 Fixing loading lists into comma separated --- custom_components/garbage_collection/config_flow.py | 3 +++ custom_components/garbage_collection/config_singularity.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/custom_components/garbage_collection/config_flow.py b/custom_components/garbage_collection/config_flow.py index 2f3cfda..b12f3c8 100644 --- a/custom_components/garbage_collection/config_flow.py +++ b/custom_components/garbage_collection/config_flow.py @@ -107,6 +107,8 @@ def step1_user_init(self, user_input: Dict, defaults=None): elif defaults is not None: config_definition.reset_defaults() config_definition.set_defaults(1, defaults) + config_definition.join_list(CONF_EXCLUDE_DATES) + config_definition.join_list(CONF_INCLUDE_DATES) self.data_schema = config_definition.compile_config_flow(step=1) # Do not show name for Options_Flow. The name cannot be changed here if defaults is not None and CONF_NAME in self.data_schema: @@ -239,6 +241,7 @@ def step4_final(self, user_input: Dict, defaults=None): return True elif defaults is not None: config_definition.set_defaults(4, defaults) + config_definition.join_list(CONF_HOLIDAY_POP_NAMED) self.data_schema = config_definition.compile_config_flow( step=4, valid_for=self._data[CONF_FREQUENCY] ) diff --git a/custom_components/garbage_collection/config_singularity.py b/custom_components/garbage_collection/config_singularity.py index 2e2bb06..7cc0a21 100644 --- a/custom_components/garbage_collection/config_singularity.py +++ b/custom_components/garbage_collection/config_singularity.py @@ -109,3 +109,8 @@ def set_defaults(self, step: int, data) -> None: type(data[key]) not in [list, dict] or len(data[key]) != 0 ): self._defaults[key] = data[key] + + def join_list(self, key: str) -> None: + """Convert a list to comma separated string.""" + if key in self._defaults and isinstance(self._defaults[key], list): + self._defaults[key] = ",".join(self._defaults[key]) From 3d70c8fc4146a4911807ad893092fef84fcf8a08 Mon Sep 17 00:00:00 2001 From: Vaclav Date: Sat, 8 Jan 2022 19:06:23 +0100 Subject: [PATCH 30/30] black --- custom_components/garbage_collection/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/garbage_collection/config_flow.py b/custom_components/garbage_collection/config_flow.py index b12f3c8..85baf76 100644 --- a/custom_components/garbage_collection/config_flow.py +++ b/custom_components/garbage_collection/config_flow.py @@ -108,7 +108,7 @@ def step1_user_init(self, user_input: Dict, defaults=None): config_definition.reset_defaults() config_definition.set_defaults(1, defaults) config_definition.join_list(CONF_EXCLUDE_DATES) - config_definition.join_list(CONF_INCLUDE_DATES) + config_definition.join_list(CONF_INCLUDE_DATES) self.data_schema = config_definition.compile_config_flow(step=1) # Do not show name for Options_Flow. The name cannot be changed here if defaults is not None and CONF_NAME in self.data_schema: