Jewish calendar binary sensor (#26200)

* Move jewish calendar to its own platform

* Fix tests for Jewish Calendar platform

As part of this, move tests to use async_setup_component instead of
testing JewishCalendarSensor as suggested by @MartinHjelmare here:

https://github.com/home-assistant/home-assistant/pull/24958#pullrequestreview-259394226

* Get sensors to update during test

* Use hass.config.set_time_zone instead of directly calling set_default_time_zone in tests

* Cleanup log messages

* Rename result from weekly_portion to parshat_hashavua

* Fix english/hebrew tests

* Fix updating of issue melacha binary sensor

* Fix docstrings of binary sensor

* Reset timezones before and after each test

* Use correct entity_id for day of the omer tests

* Fix omer tests

* Cleanup and rearrange tests

* Remove the old issur_melacha_in_effect sensor

* Rename variables to make the code clearer

Instead of using lagging_date, use after_tzais and after_shkia

* Use dt_util.set_default_time_zone instead of hass.config.set_time_zone so as not to break other tests

* Remove should_poll set to false (accidental copy/paste)

* Remove _LOGGER messaging during init and impossible cases

* Move binary tests to standalone test functions

Move sensor tests to standalone test functions

* Collect entities before calling add_entities

* Fix pylint errors

* Simplify logic in binary sensor until a future a PR adds more sensors

* Rename test_id holyness to holiday_type

* Fix time zone for binary sensor tests

Fix time zone for sensor tests

* Don't use unnecessary alter_time in sensors

Don't use unnecessary alter time in binary sensor

Remove unused alter_time

* Simply set hass.config.time_zone instead of murking around with global values

* Use async_fire_time_changed instead of directly calling async_update_entity

* Removing debug messaging during init of integration

* Capitalize constants

* Collect all Entities before calling async_add_entities

* Revert "Don't use unnecessary alter_time in sensors"

This reverts commit 74371740eaeb6e73c1a374725b05207071648ee1.

* Use test time instead of utc_now

* Remove superfluous testing

* Fix triggering of time changed

* Fix failing tests due to side-effects

* Use dt_util.as_utc instead of reimplementing it's functionality

* Use dict[key] for default values

* Move 3rd party imports to the top of the module

* Fix imports
This commit is contained in:
Tsvi Mostovicz 2019-09-06 14:24:10 +03:00 committed by Martin Hjelmare
parent 50cec91cf0
commit 815e7a70e9
6 changed files with 949 additions and 859 deletions

View File

@ -1 +1,109 @@
"""The jewish_calendar component."""
import logging
import voluptuous as vol
import hdate
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.helpers.discovery import async_load_platform
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = "jewish_calendar"
SENSOR_TYPES = {
"binary": {
"issur_melacha_in_effect": ["Issur Melacha in Effect", "mdi:power-plug-off"]
},
"data": {
"date": ["Date", "mdi:judaism"],
"weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"],
"holiday_name": ["Holiday name", "mdi:calendar-star"],
"holiday_type": ["Holiday type", "mdi:counter"],
"omer_count": ["Day of the Omer", "mdi:counter"],
},
"time": {
"first_light": ["Alot Hashachar", "mdi:weather-sunset-up"],
"gra_end_shma": ['Latest time for Shm"a GR"A', "mdi:calendar-clock"],
"mga_end_shma": ['Latest time for Shm"a MG"A', "mdi:calendar-clock"],
"plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"],
"first_stars": ["T'set Hakochavim", "mdi:weather-night"],
"upcoming_shabbat_candle_lighting": [
"Upcoming Shabbat Candle Lighting",
"mdi:candle",
],
"upcoming_shabbat_havdalah": ["Upcoming Shabbat Havdalah", "mdi:weather-night"],
"upcoming_candle_lighting": ["Upcoming Candle Lighting", "mdi:candle"],
"upcoming_havdalah": ["Upcoming Havdalah", "mdi:weather-night"],
},
}
CONF_DIASPORA = "diaspora"
CONF_LANGUAGE = "language"
CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset"
CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset"
CANDLE_LIGHT_DEFAULT = 18
DEFAULT_NAME = "Jewish Calendar"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DIASPORA, default=False): cv.boolean,
vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude,
vol.Optional(CONF_LANGUAGE, default="english"): vol.In(
["hebrew", "english"]
),
vol.Optional(
CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT
): int,
# Default of 0 means use 8.5 degrees / 'three_stars' time.
vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Set up the Jewish Calendar component."""
name = config[DOMAIN][CONF_NAME]
language = config[DOMAIN][CONF_LANGUAGE]
latitude = config[DOMAIN].get(CONF_LATITUDE, hass.config.latitude)
longitude = config[DOMAIN].get(CONF_LONGITUDE, hass.config.longitude)
diaspora = config[DOMAIN][CONF_DIASPORA]
candle_lighting_offset = config[DOMAIN][CONF_CANDLE_LIGHT_MINUTES]
havdalah_offset = config[DOMAIN][CONF_HAVDALAH_OFFSET_MINUTES]
location = hdate.Location(
latitude=latitude,
longitude=longitude,
timezone=hass.config.time_zone,
diaspora=diaspora,
)
hass.data[DOMAIN] = {
"location": location,
"name": name,
"language": language,
"candle_lighting_offset": candle_lighting_offset,
"havdalah_offset": havdalah_offset,
"diaspora": diaspora,
}
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
hass.async_create_task(
async_load_platform(hass, "binary_sensor", DOMAIN, {}, config)
)
return True

View File

@ -0,0 +1,66 @@
"""Support for Jewish Calendar binary sensors."""
import logging
import hdate
from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.util.dt as dt_util
from . import DOMAIN, SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Jewish Calendar binary sensor devices."""
if discovery_info is None:
return
async_add_entities(
[
JewishCalendarBinarySensor(hass.data[DOMAIN], sensor, sensor_info)
for sensor, sensor_info in SENSOR_TYPES["binary"].items()
]
)
class JewishCalendarBinarySensor(BinarySensorDevice):
"""Representation of an Jewish Calendar binary sensor."""
def __init__(self, data, sensor, sensor_info):
"""Initialize the binary sensor."""
self._location = data["location"]
self._type = sensor
self._name = f"{data['name']} {sensor_info[0]}"
self._icon = sensor_info[1]
self._hebrew = data["language"] == "hebrew"
self._candle_lighting_offset = data["candle_lighting_offset"]
self._havdalah_offset = data["havdalah_offset"]
self._state = False
@property
def icon(self):
"""Return the icon of the entity."""
return self._icon
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def is_on(self):
"""Return true if sensor is on."""
return self._state
async def async_update(self):
"""Update the state of the sensor."""
zmanim = hdate.Zmanim(
date=dt_util.now(),
location=self._location,
candle_lighting_offset=self._candle_lighting_offset,
havdalah_offset=self._havdalah_offset,
hebrew=self._hebrew,
)
self._state = zmanim.issur_melacha_in_effect

View File

@ -1,140 +1,59 @@
"""Platform to retrieve Jewish calendar information for Home Assistant."""
import logging
import voluptuous as vol
import hdate
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
SUN_EVENT_SUNSET,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.const import SUN_EVENT_SUNSET
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.sun import get_astral_event_date
import homeassistant.util.dt as dt_util
from . import DOMAIN, SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
"date": ["Date", "mdi:judaism"],
"weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"],
"holiday_name": ["Holiday", "mdi:calendar-star"],
"holyness": ["Holyness", "mdi:counter"],
"first_light": ["Alot Hashachar", "mdi:weather-sunset-up"],
"gra_end_shma": ['Latest time for Shm"a GR"A', "mdi:calendar-clock"],
"mga_end_shma": ['Latest time for Shm"a MG"A', "mdi:calendar-clock"],
"plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"],
"first_stars": ["T'set Hakochavim", "mdi:weather-night"],
"upcoming_shabbat_candle_lighting": [
"Upcoming Shabbat Candle Lighting",
"mdi:candle",
],
"upcoming_shabbat_havdalah": ["Upcoming Shabbat Havdalah", "mdi:weather-night"],
"upcoming_candle_lighting": ["Upcoming Candle Lighting", "mdi:candle"],
"upcoming_havdalah": ["Upcoming Havdalah", "mdi:weather-night"],
"issur_melacha_in_effect": ["Issur Melacha in Effect", "mdi:power-plug-off"],
"omer_count": ["Day of the Omer", "mdi:counter"],
}
CONF_DIASPORA = "diaspora"
CONF_LANGUAGE = "language"
CONF_SENSORS = "sensors"
CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset"
CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset"
CANDLE_LIGHT_DEFAULT = 18
DEFAULT_NAME = "Jewish Calendar"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DIASPORA, default=False): cv.boolean,
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_LANGUAGE, default="english"): vol.In(["hebrew", "english"]),
vol.Optional(CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT): int,
# Default of 0 means use 8.5 degrees / 'three_stars' time.
vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int,
vol.Optional(CONF_SENSORS, default=["date"]): vol.All(
cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]
),
}
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Jewish calendar sensor platform."""
language = config.get(CONF_LANGUAGE)
name = config.get(CONF_NAME)
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
diaspora = config.get(CONF_DIASPORA)
candle_lighting_offset = config.get(CONF_CANDLE_LIGHT_MINUTES)
havdalah_offset = config.get(CONF_HAVDALAH_OFFSET_MINUTES)
if None in (latitude, longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
if discovery_info is None:
return
dev = []
for sensor_type in config[CONF_SENSORS]:
dev.append(
JewishCalSensor(
name,
language,
sensor_type,
latitude,
longitude,
hass.config.time_zone,
diaspora,
candle_lighting_offset,
havdalah_offset,
)
)
async_add_entities(dev, True)
sensors = [
JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info)
for sensor, sensor_info in SENSOR_TYPES["data"].items()
]
sensors.extend(
JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info)
for sensor, sensor_info in SENSOR_TYPES["time"].items()
)
async_add_entities(sensors)
class JewishCalSensor(Entity):
class JewishCalendarSensor(Entity):
"""Representation of an Jewish calendar sensor."""
def __init__(
self,
name,
language,
sensor_type,
latitude,
longitude,
timezone,
diaspora,
candle_lighting_offset=CANDLE_LIGHT_DEFAULT,
havdalah_offset=0,
):
def __init__(self, data, sensor, sensor_info):
"""Initialize the Jewish calendar sensor."""
self.client_name = name
self._name = SENSOR_TYPES[sensor_type][0]
self.type = sensor_type
self._hebrew = language == "hebrew"
self._location = data["location"]
self._type = sensor
self._name = f"{data['name']} {sensor_info[0]}"
self._icon = sensor_info[1]
self._hebrew = data["language"] == "hebrew"
self._candle_lighting_offset = data["candle_lighting_offset"]
self._havdalah_offset = data["havdalah_offset"]
self._diaspora = data["diaspora"]
self._state = None
self.latitude = latitude
self.longitude = longitude
self.timezone = timezone
self.diaspora = diaspora
self.candle_lighting_offset = candle_lighting_offset
self.havdalah_offset = havdalah_offset
_LOGGER.debug("Sensor %s initialized", self.type)
@property
def name(self):
"""Return the name of the sensor."""
return f"{self.client_name} {self._name}"
return self._name
@property
def icon(self):
"""Icon to display in the front end."""
return SENSOR_TYPES[self.type][1]
return self._icon
@property
def state(self):
@ -143,9 +62,7 @@ class JewishCalSensor(Entity):
async def async_update(self):
"""Update the state of the sensor."""
import hdate
now = dt_util.as_local(dt_util.now())
now = dt_util.now()
_LOGGER.debug("Now: %s Timezone = %s", now, now.tzinfo)
today = now.date()
@ -155,66 +72,65 @@ class JewishCalSensor(Entity):
_LOGGER.debug("Now: %s Sunset: %s", now, sunset)
location = hdate.Location(
latitude=self.latitude,
longitude=self.longitude,
timezone=self.timezone,
diaspora=self.diaspora,
)
def make_zmanim(date):
"""Create a Zmanim object."""
return hdate.Zmanim(
date=date,
location=location,
candle_lighting_offset=self.candle_lighting_offset,
havdalah_offset=self.havdalah_offset,
location=self._location,
candle_lighting_offset=self._candle_lighting_offset,
havdalah_offset=self._havdalah_offset,
hebrew=self._hebrew,
)
date = hdate.HDate(today, diaspora=self.diaspora, hebrew=self._hebrew)
lagging_date = date
date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew)
# Advance Hebrew date if sunset has passed.
# Not all sensors should advance immediately when the Hebrew date
# officially changes (i.e. after sunset), hence lagging_date.
if now > sunset:
date = date.next_day
# The Jewish day starts after darkness (called "tzais") and finishes at
# sunset ("shkia"). The time in between is a gray area (aka "Bein
# Hashmashot" - literally: "in between the sun and the moon").
# For some sensors, it is more interesting to consider the date to be
# tomorrow based on sunset ("shkia"), for others based on "tzais".
# Hence the following variables.
after_tzais_date = after_shkia_date = date
today_times = make_zmanim(today)
if now > sunset:
after_shkia_date = date.next_day
if today_times.havdalah and now > today_times.havdalah:
lagging_date = lagging_date.next_day
after_tzais_date = date.next_day
# Terminology note: by convention in py-libhdate library, "upcoming"
# refers to "current" or "upcoming" dates.
if self.type == "date":
self._state = date.hebrew_date
elif self.type == "weekly_portion":
if self._type == "date":
self._state = after_shkia_date.hebrew_date
elif self._type == "weekly_portion":
# Compute the weekly portion based on the upcoming shabbat.
self._state = lagging_date.upcoming_shabbat.parasha
elif self.type == "holiday_name":
self._state = date.holiday_description
elif self.type == "holyness":
self._state = date.holiday_type
elif self.type == "upcoming_shabbat_candle_lighting":
times = make_zmanim(lagging_date.upcoming_shabbat.previous_day.gdate)
self._state = after_tzais_date.upcoming_shabbat.parasha
elif self._type == "holiday_name":
self._state = after_shkia_date.holiday_description
elif self._type == "holiday_type":
self._state = after_shkia_date.holiday_type
elif self._type == "upcoming_shabbat_candle_lighting":
times = make_zmanim(after_tzais_date.upcoming_shabbat.previous_day.gdate)
self._state = times.candle_lighting
elif self.type == "upcoming_candle_lighting":
elif self._type == "upcoming_candle_lighting":
times = make_zmanim(
lagging_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate
after_tzais_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate
)
self._state = times.candle_lighting
elif self.type == "upcoming_shabbat_havdalah":
times = make_zmanim(lagging_date.upcoming_shabbat.gdate)
elif self._type == "upcoming_shabbat_havdalah":
times = make_zmanim(after_tzais_date.upcoming_shabbat.gdate)
self._state = times.havdalah
elif self.type == "upcoming_havdalah":
times = make_zmanim(lagging_date.upcoming_shabbat_or_yom_tov.last_day.gdate)
elif self._type == "upcoming_havdalah":
times = make_zmanim(
after_tzais_date.upcoming_shabbat_or_yom_tov.last_day.gdate
)
self._state = times.havdalah
elif self.type == "issur_melacha_in_effect":
self._state = make_zmanim(now).issur_melacha_in_effect
elif self.type == "omer_count":
self._state = date.omer_day
elif self._type == "omer_count":
self._state = after_shkia_date.omer_day
else:
times = make_zmanim(today).zmanim
self._state = times[self.type].time()
self._state = times[self._type].time()
_LOGGER.debug("New value: %s", self._state)

View File

@ -1 +1,72 @@
"""Tests for the jewish_calendar component."""
from datetime import datetime
from collections import namedtuple
from contextlib import contextmanager
from unittest.mock import patch
from homeassistant.components import jewish_calendar
import homeassistant.util.dt as dt_util
_LatLng = namedtuple("_LatLng", ["lat", "lng"])
NYC_LATLNG = _LatLng(40.7128, -74.0060)
JERUSALEM_LATLNG = _LatLng(31.778, 35.235)
ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE
def teardown_module():
"""Reset time zone."""
dt_util.set_default_time_zone(ORIG_TIME_ZONE)
def make_nyc_test_params(dtime, results, havdalah_offset=0):
"""Make test params for NYC."""
if isinstance(results, dict):
time_zone = dt_util.get_time_zone("America/New_York")
results = {
key: time_zone.localize(value) if isinstance(value, datetime) else value
for key, value in results.items()
}
return (
dtime,
jewish_calendar.CANDLE_LIGHT_DEFAULT,
havdalah_offset,
True,
"America/New_York",
NYC_LATLNG.lat,
NYC_LATLNG.lng,
results,
)
def make_jerusalem_test_params(dtime, results, havdalah_offset=0):
"""Make test params for Jerusalem."""
if isinstance(results, dict):
time_zone = dt_util.get_time_zone("Asia/Jerusalem")
results = {
key: time_zone.localize(value) if isinstance(value, datetime) else value
for key, value in results.items()
}
return (
dtime,
jewish_calendar.CANDLE_LIGHT_DEFAULT,
havdalah_offset,
False,
"Asia/Jerusalem",
JERUSALEM_LATLNG.lat,
JERUSALEM_LATLNG.lng,
results,
)
@contextmanager
def alter_time(local_time):
"""Manage multiple time mocks."""
utc_time = dt_util.as_utc(local_time)
patch1 = patch("homeassistant.util.dt.utcnow", return_value=utc_time)
patch2 = patch("homeassistant.util.dt.now", return_value=local_time)
with patch1, patch2:
yield

View File

@ -0,0 +1,97 @@
"""The tests for the Jewish calendar binary sensors."""
from datetime import timedelta
from datetime import datetime as dt
import pytest
from homeassistant.const import STATE_ON, STATE_OFF
import homeassistant.util.dt as dt_util
from homeassistant.setup import async_setup_component
from homeassistant.components import jewish_calendar
from tests.common import async_fire_time_changed
from . import alter_time, make_nyc_test_params, make_jerusalem_test_params
MELACHA_PARAMS = [
make_nyc_test_params(dt(2018, 9, 1, 16, 0), STATE_ON),
make_nyc_test_params(dt(2018, 9, 1, 20, 21), STATE_OFF),
make_nyc_test_params(dt(2018, 9, 7, 13, 1), STATE_OFF),
make_nyc_test_params(dt(2018, 9, 8, 21, 25), STATE_OFF),
make_nyc_test_params(dt(2018, 9, 9, 21, 25), STATE_ON),
make_nyc_test_params(dt(2018, 9, 10, 21, 25), STATE_ON),
make_nyc_test_params(dt(2018, 9, 28, 21, 25), STATE_ON),
make_nyc_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF),
make_nyc_test_params(dt(2018, 9, 30, 21, 25), STATE_ON),
make_nyc_test_params(dt(2018, 10, 1, 21, 25), STATE_ON),
make_jerusalem_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF),
make_jerusalem_test_params(dt(2018, 9, 30, 21, 25), STATE_ON),
make_jerusalem_test_params(dt(2018, 10, 1, 21, 25), STATE_OFF),
]
MELACHA_TEST_IDS = [
"currently_first_shabbat",
"after_first_shabbat",
"friday_upcoming_shabbat",
"upcoming_rosh_hashana",
"currently_rosh_hashana",
"second_day_rosh_hashana",
"currently_shabbat_chol_hamoed",
"upcoming_two_day_yomtov_in_diaspora",
"currently_first_day_of_two_day_yomtov_in_diaspora",
"currently_second_day_of_two_day_yomtov_in_diaspora",
"upcoming_one_day_yom_tov_in_israel",
"currently_one_day_yom_tov_in_israel",
"after_one_day_yom_tov_in_israel",
]
@pytest.mark.parametrize(
[
"now",
"candle_lighting",
"havdalah",
"diaspora",
"tzname",
"latitude",
"longitude",
"result",
],
MELACHA_PARAMS,
ids=MELACHA_TEST_IDS,
)
async def test_issur_melacha_sensor(
hass, now, candle_lighting, havdalah, diaspora, tzname, latitude, longitude, result
):
"""Test Issur Melacha sensor output."""
time_zone = dt_util.get_time_zone(tzname)
test_time = time_zone.localize(now)
hass.config.time_zone = time_zone
hass.config.latitude = latitude
hass.config.longitude = longitude
with alter_time(test_time):
assert await async_setup_component(
hass,
jewish_calendar.DOMAIN,
{
"jewish_calendar": {
"name": "test",
"language": "english",
"diaspora": diaspora,
"candle_lighting_minutes_before_sunset": candle_lighting,
"havdalah_minutes_after_sunset": havdalah,
}
},
)
await hass.async_block_till_done()
future = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert (
hass.states.get("binary_sensor.test_issur_melacha_in_effect").state
== result
)

File diff suppressed because it is too large Load Diff