diff --git a/README.md b/README.md index e5ed82b..2c4a3af 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ Tilted blinds will only defect from the above approach if the inside temperature | Manual Override reset Timer | False | | Resets duration timer each time the position changes while the manual control status is active | | End Time | `"00:00:00"` | | Latest time a cover can be adjusted each day | | End Time Entity | None | | The latest moment a cover may be changed . *Overrides the `end_time` value* | +| Adjust at end time | `False` | | Make sure to always update the position to the default setting at the end time. | ### Climate diff --git a/custom_components/adaptive_cover/__init__.py b/custom_components/adaptive_cover/__init__.py index 8c6a21f..785cda0 100644 --- a/custom_components/adaptive_cover/__init__.py +++ b/custom_components/adaptive_cover/__init__.py @@ -6,17 +6,22 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.event import ( + async_track_point_in_time, async_track_state_change_event, ) from .const import ( + CONF_END_ENTITY, + CONF_END_TIME, CONF_ENTITIES, CONF_PRESENCE_ENTITY, + CONF_RETURN_SUNSET, CONF_TEMP_ENTITY, CONF_WEATHER_ENTITY, DOMAIN, ) from .coordinator import AdaptiveDataUpdateCoordinator +from .helpers import get_datetime_from_str PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.BINARY_SENSOR, Platform.BUTTON] CONF_SUN = ["sun.sun"] @@ -37,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) coordinator = AdaptiveDataUpdateCoordinator(hass) - + end_time=None _temp_entity = entry.options.get(CONF_TEMP_ENTITY) _presence_entity = entry.options.get(CONF_PRESENCE_ENTITY) _weather_entity = entry.options.get(CONF_WEATHER_ENTITY) @@ -46,6 +51,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for entity in [_temp_entity, _presence_entity, _weather_entity]: if entity is not None: _entities.append(entity) + _track_end_time = entry.options.get(CONF_RETURN_SUNSET) + _end_time = entry.options.get(CONF_END_TIME) + _end_time_entity = entry.options.get(CONF_END_ENTITY) + if _end_time is not None: + end_time = get_datetime_from_str(_end_time) + if _end_time_entity is not None: + end_time = get_datetime_from_str(_end_time_entity) entry.async_on_unload( async_track_state_change_event( @@ -63,6 +75,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + if _track_end_time and end_time is not None: + entry.async_on_unload( + async_track_point_in_time(hass, coordinator.async_timed_refresh, end_time) + ) + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator diff --git a/custom_components/adaptive_cover/config_flow.py b/custom_components/adaptive_cover/config_flow.py index 468cd80..e57ee46 100644 --- a/custom_components/adaptive_cover/config_flow.py +++ b/custom_components/adaptive_cover/config_flow.py @@ -37,6 +37,7 @@ CONF_MODE, CONF_OUTSIDETEMP_ENTITY, CONF_PRESENCE_ENTITY, + CONF_RETURN_SUNSET, CONF_SENSOR_TYPE, CONF_START_ENTITY, CONF_START_TIME, @@ -284,6 +285,7 @@ vol.Optional(CONF_END_ENTITY): selector.EntitySelector( selector.EntitySelectorConfig(domain=["sensor", "input_datetime"]) ), + vol.Optional(CONF_RETURN_SUNSET, default=False): bool } ) diff --git a/custom_components/adaptive_cover/const.py b/custom_components/adaptive_cover/const.py index f0346bc..4077bb8 100644 --- a/custom_components/adaptive_cover/const.py +++ b/custom_components/adaptive_cover/const.py @@ -48,6 +48,7 @@ CONF_START_ENTITY = "start_entity" CONF_END_TIME = "end_time" CONF_END_ENTITY = "end_entity" +CONF_RETURN_SUNSET = "return_sunset" CONF_MANUAL_OVERRIDE_DURATION = "manual_override_duration" CONF_MANUAL_OVERRIDE_RESET = "manual_override_reset" diff --git a/custom_components/adaptive_cover/coordinator.py b/custom_components/adaptive_cover/coordinator.py index 99cadfd..2cf9354 100644 --- a/custom_components/adaptive_cover/coordinator.py +++ b/custom_components/adaptive_cover/coordinator.py @@ -111,6 +111,7 @@ def __init__(self, hass: HomeAssistant) -> None: # noqa: D107 self.state_change = False self.cover_state_change = False self.first_refresh = False + self.timed_refresh = False self.state_change_data: StateChangedData | None = None self.manager = AdaptiveCoverManager(self.manual_duration) self.wait_for_target = {} @@ -122,6 +123,21 @@ async def async_config_entry_first_refresh(self) -> None: await super().async_config_entry_first_refresh() _LOGGER.debug("Config entry first refresh") + async def async_timed_refresh(self, event) -> None: + """Control state at end time.""" + + if self.end_time is not None: + time = self.end_time + if self.end_time_entity is not None: + time = get_safe_state(self.hass, self.end_time_entity) + time_check = dt.datetime.now() + dt.timedelta(hours=2) - get_datetime_from_str(time) + if time is not None and (time_check <= dt.timedelta(seconds=1)) : + self.timed_refresh = True + _LOGGER.debug("Timed refresh triggered") + await self.async_refresh() + else: + _LOGGER.debug("Time not equal to end time") + async def async_check_entity_state_change( self, event: Event[EventStateChangedData] ) -> None: @@ -222,7 +238,7 @@ async def _async_update_data(self) -> AdaptiveCoverData: await self.manager.reset_if_needed() - if self.control_toggle and self.state_change: + if self.state_change and self.control_toggle: for cover in self.entities: await self.async_handle_call_service(cover) self.state_change = False @@ -237,6 +253,12 @@ async def _async_update_data(self) -> AdaptiveCoverData: await self.async_set_position(cover) self.first_refresh = False + if self.timed_refresh and self.control_toggle: + for cover in self.entities: + await self.async_set_manual_position(cover, self.config_entry.options.get(CONF_SUNSET_POS)) + self.timed_refresh = False + + return AdaptiveCoverData( climate_mode_toggle=self.switch_mode, states={ @@ -271,6 +293,10 @@ async def async_handle_call_service(self, entity): await self.async_set_position(entity) async def async_set_position(self, entity): + """Call service to set cover position.""" + await self.async_set_manual_position(entity, self.state) + + async def async_set_manual_position(self, entity, position): """Call service to set cover position.""" service = SERVICE_SET_COVER_POSITION service_data = {} @@ -278,12 +304,12 @@ async def async_set_position(self, entity): if self._cover_type == "cover_tilt": service = SERVICE_SET_COVER_TILT_POSITION - service_data[ATTR_TILT_POSITION] = self.state + service_data[ATTR_TILT_POSITION] = position else: - service_data[ATTR_POSITION] = self.state + service_data[ATTR_POSITION] = position self.wait_for_target[entity] = True - self.target_call[entity] = self.state + self.target_call[entity] = position await self.hass.services.async_call(COVER_DOMAIN, service, service_data) _LOGGER.debug("Run %s with data %s", service, service_data) @@ -334,12 +360,12 @@ def before_end_time(self): time = get_datetime_from_str( get_safe_state(self.hass, self.end_time_entity) ) - now = dt.datetime.now(dt.UTC) - return now <= time + now = dt.datetime.now() + return now < time if self.end_time is not None: time = get_datetime_from_str(self.end_time).time() now = dt.datetime.now().time() - return now <= time + return now < time return True def check_position(self, entity): diff --git a/custom_components/adaptive_cover/strings.json b/custom_components/adaptive_cover/strings.json index 1d90143..fb6076b 100644 --- a/custom_components/adaptive_cover/strings.json +++ b/custom_components/adaptive_cover/strings.json @@ -18,7 +18,8 @@ "manual_override_duration": "Duration of manual override", "manual_override_reset": "Reset Manual override duration", "end_time": "End time", - "end_entity": "Entity indicating ending time" + "end_entity": "Entity indicating ending time", + "return_sunset": "Always adjust position to sunset default at end time; Useful if end time is before actual sunset" }, "data_description": { "delta_position": "Minimum change in position required before adjusting the cover's position", @@ -185,7 +186,8 @@ "manual_override_duration": "Duration of manual override", "manual_override_reset": "Reset Manual override duration", "end_time": "End time", - "end_entity": "Entity indicating ending time" + "end_entity": "Entity indicating ending time", + "return_sunset": "Always adjust position to sunset default at end time; Useful if end time is before actual sunset" }, "data_description": { "delta_position": "Minimum change in position required before adjusting the cover's position", @@ -195,7 +197,7 @@ "manual_override_duration": "The duration of manual control before resetting to automatic control", "manual_override_reset": "Option to reset the manual override duration after each manual adjustment; if disabled, the duration only applies to the first manual adjustment", "end_time": "Ending time for each day; after this time, the cover will remain stationary", - "end_entity": "Entity representing ending time state, overriding the fixed end time set above. Useful for automating the end time with an entity" + "end_entity": "Ensure that the position is consistently adjusted to the default sunset setting by the end time. This is particularly beneficial when the end time precedes the actual sunset." } }, "vertical": { diff --git a/custom_components/adaptive_cover/switch.py b/custom_components/adaptive_cover/switch.py index 6a46588..65c5445 100644 --- a/custom_components/adaptive_cover/switch.py +++ b/custom_components/adaptive_cover/switch.py @@ -135,7 +135,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: setattr(self.coordinator, self._key, True) if self._key == "control_toggle" and kwargs.get("added") is not True: for entity in self.coordinator.entities: - if not self.coordinator.manager.is_cover_manual(entity) and self.coordinator.after_start_time: + if not self.coordinator.manager.is_cover_manual(entity) and self.coordinator.check_adaptive_time: await self.coordinator.async_set_position(entity) await self.coordinator.async_refresh() self.schedule_update_ha_state() diff --git a/custom_components/adaptive_cover/translations/en.json b/custom_components/adaptive_cover/translations/en.json index 1d90143..fb6076b 100644 --- a/custom_components/adaptive_cover/translations/en.json +++ b/custom_components/adaptive_cover/translations/en.json @@ -18,7 +18,8 @@ "manual_override_duration": "Duration of manual override", "manual_override_reset": "Reset Manual override duration", "end_time": "End time", - "end_entity": "Entity indicating ending time" + "end_entity": "Entity indicating ending time", + "return_sunset": "Always adjust position to sunset default at end time; Useful if end time is before actual sunset" }, "data_description": { "delta_position": "Minimum change in position required before adjusting the cover's position", @@ -185,7 +186,8 @@ "manual_override_duration": "Duration of manual override", "manual_override_reset": "Reset Manual override duration", "end_time": "End time", - "end_entity": "Entity indicating ending time" + "end_entity": "Entity indicating ending time", + "return_sunset": "Always adjust position to sunset default at end time; Useful if end time is before actual sunset" }, "data_description": { "delta_position": "Minimum change in position required before adjusting the cover's position", @@ -195,7 +197,7 @@ "manual_override_duration": "The duration of manual control before resetting to automatic control", "manual_override_reset": "Option to reset the manual override duration after each manual adjustment; if disabled, the duration only applies to the first manual adjustment", "end_time": "Ending time for each day; after this time, the cover will remain stationary", - "end_entity": "Entity representing ending time state, overriding the fixed end time set above. Useful for automating the end time with an entity" + "end_entity": "Ensure that the position is consistently adjusted to the default sunset setting by the end time. This is particularly beneficial when the end time precedes the actual sunset." } }, "vertical": { diff --git a/custom_components/adaptive_cover/translations/nl.json b/custom_components/adaptive_cover/translations/nl.json index 9525be8..47f4c53 100644 --- a/custom_components/adaptive_cover/translations/nl.json +++ b/custom_components/adaptive_cover/translations/nl.json @@ -18,7 +18,8 @@ "manual_override_duration": "Duur van handmatige overschrijving", "manual_override_reset": "Reset handmatige overschrijvingsduur", "end_time": "Eindtijd", - "end_entity": "Entiteit die eindtijd aangeeft" + "end_entity": "Entiteit die eindtijd aangeeft", + "return_sunset": "Zorg ervoor dat de positie altijd wordt aangepast naar de standaard zonsonderganginstelling tegen het eindtijdstip. Dit is vooral handig wanneer het eindtijdstip voor de daadwerkelijke zonsondergang valt." }, "data_description": { "delta_position": "Minimale verandering in positie vereist voordat de positie van de bekleding wordt aangepast", @@ -190,7 +191,10 @@ "start_time": "Starttijd voor elke dag; voor deze tijd blijft de bekleding stationair", "start_entity": "Entiteit die de status van de starttijd vertegenwoordigt, die de hierboven ingestelde statische starttijd overschrijft. Handig voor het automatiseren van de starttijd met een entiteit", "manual_override_duration": "De duur van handmatige bediening voordat wordt teruggekeerd naar automatische bediening", - "manual_override_reset": "Optie om de duur van handmatige overschrijving na elke handmatige aanpassing te resetten; als dit is uitgeschakeld, geldt de duur alleen voor de eerste handmatige aanpassing" + "manual_override_reset": "Optie om de duur van handmatige overschrijving na elke handmatige aanpassing te resetten; als dit is uitgeschakeld, geldt de duur alleen voor de eerste handmatige aanpassing", + "end_time": "Eindtijd", + "end_entity": "Entiteit die eindtijd aangeeft", + "return_sunset": "Zorg ervoor dat de positie altijd wordt aangepast naar de standaard zonsonderganginstelling tegen het eindtijdstip. Dit is vooral handig wanneer het eindtijdstip voor de daadwerkelijke zonsondergang valt." } }, "vertical": {