diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 04a3a2544c1..1963359bf0a 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import date, timedelta +from datetime import date, datetime, timedelta from typing import Final from holidays import ( @@ -15,13 +15,20 @@ import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME -from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse +from homeassistant.core import ( + CALLBACK_TYPE, + HomeAssistant, + ServiceResponse, + SupportsResponse, + callback, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, ) +from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.util import dt as dt_util, slugify @@ -201,6 +208,8 @@ class IsWorkdaySensor(BinarySensorEntity): _attr_has_entity_name = True _attr_name = None _attr_translation_key = DOMAIN + _attr_should_poll = False + unsub: CALLBACK_TYPE | None = None def __init__( self, @@ -248,11 +257,34 @@ class IsWorkdaySensor(BinarySensorEntity): return False - async def async_update(self) -> None: - """Get date and look whether it is a holiday.""" - self._attr_is_on = self.date_is_workday(dt_util.now()) + def get_next_interval(self, now: datetime) -> datetime: + """Compute next time an update should occur.""" + tomorrow = dt_util.as_local(now) + timedelta(days=1) + return dt_util.start_of_local_day(tomorrow) - async def check_date(self, check_date: date) -> ServiceResponse: + def _update_state_and_setup_listener(self) -> None: + """Update state and setup listener for next interval.""" + now = dt_util.utcnow() + self.update_data(now) + self.unsub = async_track_point_in_utc_time( + self.hass, self.point_in_time_listener, self.get_next_interval(now) + ) + + @callback + def point_in_time_listener(self, time_date: datetime) -> None: + """Get the latest data and update state.""" + self._update_state_and_setup_listener() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Set up first update.""" + self._update_state_and_setup_listener() + + def update_data(self, now: datetime) -> None: + """Get date and look whether it is a holiday.""" + self._attr_is_on = self.date_is_workday(now) + + def check_date(self, check_date: date) -> ServiceResponse: """Service to check if date is workday or not.""" return {"workday": self.date_is_workday(check_date)} diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index a3fba852f60..e9f0e8023bc 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -1,6 +1,6 @@ """Tests the Home Assistant workday binary sensor.""" -from datetime import date, datetime +from datetime import date, datetime, timedelta from typing import Any from freezegun.api import FrozenDateTimeFactory @@ -41,31 +41,34 @@ from . import ( init_integration, ) +from tests.common import async_fire_time_changed + @pytest.mark.parametrize( - ("config", "expected_state"), + ("config", "expected_state", "expected_state_weekend"), [ - (TEST_CONFIG_NO_COUNTRY, "on"), - (TEST_CONFIG_WITH_PROVINCE, "off"), - (TEST_CONFIG_NO_PROVINCE, "off"), - (TEST_CONFIG_WITH_STATE, "on"), - (TEST_CONFIG_NO_STATE, "on"), - (TEST_CONFIG_EXAMPLE_1, "on"), - (TEST_CONFIG_EXAMPLE_2, "off"), - (TEST_CONFIG_TOMORROW, "off"), - (TEST_CONFIG_DAY_AFTER_TOMORROW, "off"), - (TEST_CONFIG_YESTERDAY, "on"), - (TEST_CONFIG_NO_LANGUAGE_CONFIGURED, "off"), + (TEST_CONFIG_NO_COUNTRY, "on", "off"), + (TEST_CONFIG_WITH_PROVINCE, "off", "off"), + (TEST_CONFIG_NO_PROVINCE, "off", "off"), + (TEST_CONFIG_WITH_STATE, "on", "off"), + (TEST_CONFIG_NO_STATE, "on", "off"), + (TEST_CONFIG_EXAMPLE_1, "on", "off"), + (TEST_CONFIG_EXAMPLE_2, "off", "off"), + (TEST_CONFIG_TOMORROW, "off", "off"), + (TEST_CONFIG_DAY_AFTER_TOMORROW, "off", "off"), + (TEST_CONFIG_YESTERDAY, "on", "off"), # Friday was good Friday + (TEST_CONFIG_NO_LANGUAGE_CONFIGURED, "off", "off"), ], ) async def test_setup( hass: HomeAssistant, config: dict[str, Any], expected_state: str, + expected_state_weekend: str, freezer: FrozenDateTimeFactory, ) -> None: """Test setup from various configs.""" - freezer.move_to(datetime(2022, 4, 15, 12, tzinfo=UTC)) # Monday + freezer.move_to(datetime(2022, 4, 15, 12, tzinfo=UTC)) # Friday await init_integration(hass, config) state = hass.states.get("binary_sensor.workday_sensor") @@ -78,6 +81,13 @@ async def test_setup( "days_offset": config["days_offset"], } + freezer.tick(timedelta(days=1)) # Saturday + async_fire_time_changed(hass) + + state = hass.states.get("binary_sensor.workday_sensor") + assert state is not None + assert state.state == expected_state_weekend + async def test_setup_with_invalid_province_from_yaml(hass: HomeAssistant) -> None: """Test setup invalid province with import."""