* Moved climate components with tests into platform dirs. * Updated tests from climate component. * Moved binary_sensor components with tests into platform dirs. * Updated tests from binary_sensor component. * Moved calendar components with tests into platform dirs. * Updated tests from calendar component. * Moved camera components with tests into platform dirs. * Updated tests from camera component. * Moved cover components with tests into platform dirs. * Updated tests from cover component. * Moved device_tracker components with tests into platform dirs. * Updated tests from device_tracker component. * Moved fan components with tests into platform dirs. * Updated tests from fan component. * Moved geo_location components with tests into platform dirs. * Updated tests from geo_location component. * Moved image_processing components with tests into platform dirs. * Updated tests from image_processing component. * Moved light components with tests into platform dirs. * Updated tests from light component. * Moved lock components with tests into platform dirs. * Moved media_player components with tests into platform dirs. * Updated tests from media_player component. * Moved scene components with tests into platform dirs. * Moved sensor components with tests into platform dirs. * Updated tests from sensor component. * Moved switch components with tests into platform dirs. * Updated tests from sensor component. * Moved vacuum components with tests into platform dirs. * Updated tests from vacuum component. * Moved weather components with tests into platform dirs. * Fixed __init__.py files * Fixes for stuff moved as part of this branch. * Fix stuff needed to merge with balloob's branch. * Formatting issues. * Missing __init__.py files. * Fix-ups * Fixup * Regenerated requirements. * Linting errors fixed. * Fixed more broken tests. * Missing init files. * Fix broken tests. * More broken tests * There seems to be a thread race condition. I suspect the logger stuff is running in another thread, which means waiting until the aio loop is done is missing the log messages. Used sleep instead because that allows the logger thread to run. I think the api_streams sensor might not be thread safe. * Disabled tests, will remove sensor in #22147 * Updated coverage and codeowners.
223 lines
8 KiB
Python
223 lines
8 KiB
Python
"""Support for representing current time of the day as binary sensors."""
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
|
|
import pytz
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.binary_sensor import (
|
|
PLATFORM_SCHEMA, BinarySensorDevice)
|
|
from homeassistant.const import (
|
|
CONF_AFTER, CONF_BEFORE, CONF_NAME, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET)
|
|
from homeassistant.core import callback
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
|
from homeassistant.helpers.sun import (
|
|
get_astral_event_date, get_astral_event_next)
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
ATTR_AFTER = 'after'
|
|
ATTR_BEFORE = 'before'
|
|
ATTR_NEXT_UPDATE = 'next_update'
|
|
|
|
CONF_AFTER_OFFSET = 'after_offset'
|
|
CONF_BEFORE_OFFSET = 'before_offset'
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
vol.Required(CONF_AFTER):
|
|
vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)),
|
|
vol.Required(CONF_BEFORE):
|
|
vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)),
|
|
vol.Required(CONF_NAME): cv.string,
|
|
vol.Optional(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_period,
|
|
vol.Optional(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_period,
|
|
})
|
|
|
|
|
|
async def async_setup_platform(
|
|
hass, config, async_add_entities, discovery_info=None):
|
|
"""Set up the ToD sensors."""
|
|
if hass.config.time_zone is None:
|
|
_LOGGER.error("Timezone is not set in Home Assistant configuration")
|
|
return
|
|
|
|
after = config[CONF_AFTER]
|
|
after_offset = config[CONF_AFTER_OFFSET]
|
|
before = config[CONF_BEFORE]
|
|
before_offset = config[CONF_BEFORE_OFFSET]
|
|
name = config[CONF_NAME]
|
|
sensor = TodSensor(name, after, after_offset, before, before_offset)
|
|
|
|
async_add_entities([sensor])
|
|
|
|
|
|
def is_sun_event(event):
|
|
"""Return true if event is sun event not time."""
|
|
return event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET)
|
|
|
|
|
|
class TodSensor(BinarySensorDevice):
|
|
"""Time of the Day Sensor."""
|
|
|
|
def __init__(self, name, after, after_offset, before, before_offset):
|
|
"""Init the ToD Sensor..."""
|
|
self._name = name
|
|
self._time_before = self._time_after = self._next_update = None
|
|
self._after_offset = after_offset
|
|
self._before_offset = before_offset
|
|
self._before = before
|
|
self._after = after
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Sensor does not need to be polled."""
|
|
return False
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return self._name
|
|
|
|
@property
|
|
def after(self):
|
|
"""Return the timestamp for the begining of the period."""
|
|
return self._time_after
|
|
|
|
@property
|
|
def before(self):
|
|
"""Return the timestamp for the end of the period."""
|
|
return self._time_before
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return True is sensor is on."""
|
|
if self.after < self.before:
|
|
return self.after <= self.current_datetime < self.before
|
|
return False
|
|
|
|
@property
|
|
def current_datetime(self):
|
|
"""Return local current datetime according to hass configuration."""
|
|
return dt_util.utcnow()
|
|
|
|
@property
|
|
def next_update(self):
|
|
"""Return the next update point in the UTC time."""
|
|
return self._next_update
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the state attributes of the sensor."""
|
|
return {
|
|
ATTR_AFTER: self.after.astimezone(
|
|
self.hass.config.time_zone).isoformat(),
|
|
ATTR_BEFORE: self.before.astimezone(
|
|
self.hass.config.time_zone).isoformat(),
|
|
ATTR_NEXT_UPDATE: self.next_update.astimezone(
|
|
self.hass.config.time_zone).isoformat(),
|
|
}
|
|
|
|
def _naive_time_to_utc_datetime(self, naive_time):
|
|
"""Convert naive time from config to utc_datetime with current day."""
|
|
# get the current local date from utc time
|
|
current_local_date = self.current_datetime.astimezone(
|
|
self.hass.config.time_zone).date()
|
|
# calcuate utc datetime corecponding to local time
|
|
utc_datetime = self.hass.config.time_zone.localize(
|
|
datetime.combine(
|
|
current_local_date, naive_time)).astimezone(tz=pytz.UTC)
|
|
return utc_datetime
|
|
|
|
def _calculate_initial_boudary_time(self):
|
|
"""Calculate internal absolute time boudaries."""
|
|
nowutc = self.current_datetime
|
|
# If after value is a sun event instead of absolute time
|
|
if is_sun_event(self._after):
|
|
# Calculate the today's event utc time or
|
|
# if not available take next
|
|
after_event_date = \
|
|
get_astral_event_date(self.hass, self._after, nowutc) or \
|
|
get_astral_event_next(self.hass, self._after, nowutc)
|
|
else:
|
|
# Convert local time provided to UTC today
|
|
# datetime.combine(date, time, tzinfo) is not supported
|
|
# in python 3.5. The self._after is provided
|
|
# with hass configured TZ not system wide
|
|
after_event_date = self._naive_time_to_utc_datetime(self._after)
|
|
|
|
self._time_after = after_event_date
|
|
|
|
# If before value is a sun event instead of absolute time
|
|
if is_sun_event(self._before):
|
|
# Calculate the today's event utc time or if not available take
|
|
# next
|
|
before_event_date = \
|
|
get_astral_event_date(self.hass, self._before, nowutc) or \
|
|
get_astral_event_next(self.hass, self._before, nowutc)
|
|
# Before is earlier than after
|
|
if before_event_date < after_event_date:
|
|
# Take next day for before
|
|
before_event_date = get_astral_event_next(
|
|
self.hass, self._before, after_event_date)
|
|
else:
|
|
# Convert local time provided to UTC today, see above
|
|
before_event_date = self._naive_time_to_utc_datetime(self._before)
|
|
|
|
# It is safe to add timedelta days=1 to UTC as there is no DST
|
|
if before_event_date < after_event_date + self._after_offset:
|
|
before_event_date += timedelta(days=1)
|
|
|
|
self._time_before = before_event_date
|
|
|
|
# Add offset to utc boundaries according to the configuration
|
|
self._time_after += self._after_offset
|
|
self._time_before += self._before_offset
|
|
|
|
def _turn_to_next_day(self):
|
|
"""Turn to to the next day."""
|
|
if is_sun_event(self._after):
|
|
self._time_after = get_astral_event_next(
|
|
self.hass, self._after,
|
|
self._time_after - self._after_offset)
|
|
self._time_after += self._after_offset
|
|
else:
|
|
# Offset is already there
|
|
self._time_after += timedelta(days=1)
|
|
|
|
if is_sun_event(self._before):
|
|
self._time_before = get_astral_event_next(
|
|
self.hass, self._before,
|
|
self._time_before - self._before_offset)
|
|
self._time_before += self._before_offset
|
|
else:
|
|
# Offset is already there
|
|
self._time_before += timedelta(days=1)
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Call when entity about to be added to Home Assistant."""
|
|
self._calculate_initial_boudary_time()
|
|
self._calculate_next_update()
|
|
self._point_in_time_listener(dt_util.now())
|
|
|
|
def _calculate_next_update(self):
|
|
"""Datetime when the next update to the state."""
|
|
now = self.current_datetime
|
|
if now < self.after:
|
|
self._next_update = self.after
|
|
return
|
|
if now < self.before:
|
|
self._next_update = self.before
|
|
return
|
|
self._turn_to_next_day()
|
|
self._next_update = self.after
|
|
|
|
@callback
|
|
def _point_in_time_listener(self, now):
|
|
"""Run when the state of the sensor should be updated."""
|
|
self._calculate_next_update()
|
|
self.async_schedule_update_ha_state()
|
|
|
|
async_track_point_in_utc_time(
|
|
self.hass, self._point_in_time_listener, self.next_update)
|