Added workday sensor (#6599)

* Added workday sensor

* Added unit tests
This commit is contained in:
Wolf-Bastian Pöttner 2017-03-16 07:46:13 +01:00 committed by Paulus Schoutsen
parent 774fd19638
commit 509cfb6433
4 changed files with 314 additions and 0 deletions

View file

@ -0,0 +1,157 @@
"""Sensor to indicate whether the current day is a workday."""
import asyncio
import logging
import datetime
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN,
CONF_NAME, WEEKDAYS)
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.8.1']
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Canada', 'CA',
'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'England',
'EuropeanCentralBank', 'ECB', 'TAR', 'Germany', 'DE',
'Ireland', 'Isle of Man', 'Mexico', 'MX', 'Netherlands', 'NL',
'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO',
'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Spain',
'ES', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales']
CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays'
# By default, Monday - Friday are workdays
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
CONF_EXCLUDES = 'excludes'
# By default, public holidays, Saturdays and Sundays are excluded from workdays
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
DEFAULT_NAME = 'Workday Sensor'
ALLOWED_DAYS = WEEKDAYS + ['holiday']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
vol.Optional(CONF_PROVINCE, default=None): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Workday sensor."""
import holidays
# Get the Sensor name from the config
sensor_name = config.get(CONF_NAME)
# Get the country code from the config
country = config.get(CONF_COUNTRY)
# Get the province from the config
province = config.get(CONF_PROVINCE)
# Get the list of workdays from the config
workdays = config.get(CONF_WORKDAYS)
# Get the list of excludes from the config
excludes = config.get(CONF_EXCLUDES)
# Instantiate the holidays module for the current year
year = datetime.datetime.now().year
obj_holidays = getattr(holidays, country)(years=year)
# Also apply the provience, if available for the configured country
if province:
if province not in obj_holidays.PROVINCES:
_LOGGER.error('There is no province/state %s in country %s',
province, country)
return False
else:
year = datetime.datetime.now().year
obj_holidays = getattr(holidays, country)(prov=province,
years=year)
# Output found public holidays via the debug channel
_LOGGER.debug("Found the following holidays for your configuration:")
for date, name in sorted(obj_holidays.items()):
_LOGGER.debug("%s %s", date, name)
# Add ourselves as device
add_devices([IsWorkdaySensor(obj_holidays, workdays,
excludes, sensor_name)], True)
def day_to_string(day):
"""Convert day index 0 - 7 to string."""
try:
return ALLOWED_DAYS[day]
except IndexError:
return None
class IsWorkdaySensor(Entity):
"""Implementation of a Workday sensor."""
def __init__(self, obj_holidays, workdays, excludes, name):
"""Initialize the Workday sensor."""
self._name = name
self._obj_holidays = obj_holidays
self._workdays = workdays
self._excludes = excludes
self._state = STATE_UNKNOWN
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
def is_include(self, day, now):
"""Check if given day is in the includes list."""
# Check includes
if day in self._workdays:
return True
elif 'holiday' in self._workdays and now in self._obj_holidays:
return True
return False
def is_exclude(self, day, now):
"""Check if given day is in the excludes list."""
# Check excludes
if day in self._excludes:
return True
elif 'holiday' in self._excludes and now in self._obj_holidays:
return True
return False
@asyncio.coroutine
def async_update(self):
"""Get date and look whether it is a holiday."""
# Default is no workday
self._state = STATE_OFF
# Get iso day of the week (1 = Monday, 7 = Sunday)
day = datetime.datetime.today().isoweekday() - 1
day_of_week = day_to_string(day)
if self.is_include(day_of_week, dt_util.now()):
self._state = STATE_ON
if self.is_exclude(day_of_week, dt_util.now()):
self._state = STATE_OFF

View file

@ -215,6 +215,9 @@ heatmiserV3==0.9.1
# homeassistant.components.switch.hikvisioncam
hikvision==0.4
# homeassistant.components.binary_sensor.workday
holidays==0.8.1
# homeassistant.components.sensor.dht
# http://github.com/adafruit/Adafruit_Python_DHT/archive/da8cddf7fb629c1ef4f046ca44f42523c9cf2d11.zip#Adafruit_DHT==1.3.0

View file

@ -17,3 +17,4 @@ requests_mock>=1.0
mock-open>=1.3.1
flake8-docstrings==1.0.2
asynctest>=0.8.0
freezegun>=0.3.8

View file

@ -0,0 +1,153 @@
"""Tests the HASS workday binary sensor."""
from freezegun import freeze_time
from homeassistant.components.binary_sensor.workday import day_to_string
from homeassistant.setup import setup_component
from tests.common import (
get_test_home_assistant, assert_setup_component)
class TestWorkdaySetup(object):
"""Test class for workday sensor."""
def setup_method(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
# Set valid default config for test
self.config_province = {
'binary_sensor': {
'platform': 'workday',
'country': 'DE',
'province': 'BW'
},
}
self.config_noprovince = {
'binary_sensor': {
'platform': 'workday',
'country': 'DE',
},
}
self.config_invalidprovince = {
'binary_sensor': {
'platform': 'workday',
'country': 'DE',
'province': 'invalid'
},
}
self.config_includeholiday = {
'binary_sensor': {
'platform': 'workday',
'country': 'DE',
'province': 'BW',
'workdays': ['holiday', 'mon', 'tue', 'wed', 'thu', 'fri'],
'excludes': ['sat', 'sun']
},
}
def teardown_method(self):
"""Stop everything that was started."""
self.hass.stop()
def test_setup_component_province(self):
"""Setup workday component."""
with assert_setup_component(1, 'binary_sensor'):
setup_component(self.hass, 'binary_sensor', self.config_province)
assert self.hass.states.get('binary_sensor.workday_sensor') is not None
# Freeze time to a workday
@freeze_time("Mar 15th, 2017")
def test_workday_province(self):
"""Test if workdays are reported correctly."""
with assert_setup_component(1, 'binary_sensor'):
setup_component(self.hass, 'binary_sensor', self.config_province)
assert self.hass.states.get('binary_sensor.workday_sensor') is not None
self.hass.start()
entity = self.hass.states.get('binary_sensor.workday_sensor')
assert entity.state == 'on'
# Freeze time to a weekend
@freeze_time("Mar 12th, 2017")
def test_weekend_province(self):
"""Test if weekends are reported correctly."""
with assert_setup_component(1, 'binary_sensor'):
setup_component(self.hass, 'binary_sensor', self.config_province)
assert self.hass.states.get('binary_sensor.workday_sensor') is not None
self.hass.start()
entity = self.hass.states.get('binary_sensor.workday_sensor')
assert entity.state == 'off'
# Freeze time to a public holiday in province BW
@freeze_time("Jan 6th, 2017")
def test_public_holiday_province(self):
"""Test if public holidays are reported correctly."""
with assert_setup_component(1, 'binary_sensor'):
setup_component(self.hass, 'binary_sensor', self.config_province)
assert self.hass.states.get('binary_sensor.workday_sensor') is not None
self.hass.start()
entity = self.hass.states.get('binary_sensor.workday_sensor')
assert entity.state == 'off'
def test_setup_component_noprovince(self):
"""Setup workday component."""
with assert_setup_component(1, 'binary_sensor'):
setup_component(self.hass, 'binary_sensor', self.config_noprovince)
assert self.hass.states.get('binary_sensor.workday_sensor') is not None
# Freeze time to a public holiday in province BW
@freeze_time("Jan 6th, 2017")
def test_public_holiday_noprovince(self):
"""Test if public holidays are reported correctly."""
with assert_setup_component(1, 'binary_sensor'):
setup_component(self.hass, 'binary_sensor', self.config_noprovince)
assert self.hass.states.get('binary_sensor.workday_sensor') is not None
self.hass.start()
entity = self.hass.states.get('binary_sensor.workday_sensor')
assert entity.state == 'on'
def test_setup_component_invalidprovince(self):
"""Setup workday component."""
with assert_setup_component(1, 'binary_sensor'):
setup_component(self.hass, 'binary_sensor',
self.config_invalidprovince)
assert self.hass.states.get('binary_sensor.workday_sensor') is None
# Freeze time to a public holiday in province BW
@freeze_time("Jan 6th, 2017")
def test_public_holiday_includeholiday(self):
"""Test if public holidays are reported correctly."""
with assert_setup_component(1, 'binary_sensor'):
setup_component(self.hass, 'binary_sensor',
self.config_includeholiday)
assert self.hass.states.get('binary_sensor.workday_sensor') is not None
self.hass.start()
entity = self.hass.states.get('binary_sensor.workday_sensor')
assert entity.state == 'on'
def test_day_to_string(self):
"""Test if day_to_string is behaving correctly."""
assert day_to_string(0) == 'mon'
assert day_to_string(1) == 'tue'
assert day_to_string(7) == 'holiday'
assert day_to_string(8) is None