""" Sensor """ from dateutil.relativedelta import relativedelta from datetime import datetime, date import logging from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.helpers import template as templater import homeassistant.util.dt as dt_util from .calendar import EntitiesCalendarData from homeassistant.helpers.discovery import async_load_platform from homeassistant.const import ( CONF_NAME, ATTR_ATTRIBUTION, ) _LOGGER = logging.getLogger(__name__) from .const import ( ATTRIBUTION, DEFAULT_UNIT_OF_MEASUREMENT, CONF_ICON_NORMAL, CONF_ICON_TODAY, CONF_ICON_SOON, CONF_DATE, CONF_DATE_TEMPLATE, CONF_SOON, CONF_HALF_ANNIVERSARY, CONF_UNIT_OF_MEASUREMENT, CONF_ID_PREFIX, CONF_ONE_TIME, CONF_COUNT_UP, DOMAIN, SENSOR_PLATFORM, CALENDAR_PLATFORM, CALENDAR_NAME, ) ATTR_YEARS_NEXT = "years_at_anniversary" ATTR_YEARS_CURRENT = "current_years" ATTR_DATE = "date" ATTR_NEXT_DATE = "next_date" ATTR_WEEKS = "weeks_remaining" ATTR_HALF_DATE = "half_anniversary_date" ATTR_HALF_DAYS = "days_until_half_anniversary" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Setup the sensor platform.""" async_add_entities([anniversaries(hass, discovery_info)], True) async def async_setup_entry(hass, config_entry, async_add_devices): """Setup sensor platform.""" async_add_devices([anniversaries(hass, config_entry.data)], True) def validate_date(value): try: return datetime.strptime(value, "%Y-%m-%d"), False except ValueError: pass try: return datetime.strptime(value, "%m-%d"), True except ValueError: return "Invalid Date", False class anniversaries(Entity): def __init__(self, hass, config): """Initialize the sensor.""" self.config = config self._name = config.get(CONF_NAME) self._id_prefix = config.get(CONF_ID_PREFIX) if self._id_prefix is None: self._id_prefix = "anniversary_" self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, self._id_prefix + self._name, []) self._unknown_year = False self._date = "" self._show_half_anniversary = config.get(CONF_HALF_ANNIVERSARY) self._half_days_remaining = 0 self._half_date = "" self._template_sensor = False self._date_template = config.get(CONF_DATE_TEMPLATE) if self._date_template is not None: self._template_sensor = True else: self._date, self._unknown_year = validate_date(config.get(CONF_DATE)) self._date = self._date.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE) if self._show_half_anniversary: self._half_date = self._date + relativedelta(months=+6) self._icon_normal = config.get(CONF_ICON_NORMAL) self._icon_today = config.get(CONF_ICON_TODAY) self._icon_soon = config.get(CONF_ICON_SOON) self._soon = config.get(CONF_SOON) self._icon = self._icon_normal self._years_next = 0 self._years_current = 0 self._state = 0 self._weeks_remaining = 0 self._unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) if self._unit_of_measurement is None: self._unit_of_measurement = DEFAULT_UNIT_OF_MEASUREMENT self._one_time = config.get(CONF_ONE_TIME) self._count_up = config.get(CONF_COUNT_UP) @property def unique_id(self): """Return a unique ID to use for this sensor.""" return self.config.get("unique_id", None) @property def name(self): """Return the name of the sensor.""" return self._name @property def state(self): """Return the name of the sensor.""" return self._state @property def extra_state_attributes(self): """Return the state attributes.""" res = {} res[ATTR_ATTRIBUTION] = ATTRIBUTION if self._state in ["Invalid Date", "Invalid Template"]: return res if not self._unknown_year: res[ATTR_YEARS_NEXT] = self._years_next res[ATTR_YEARS_CURRENT] = self._years_current res[ATTR_DATE] = self._date res[ATTR_NEXT_DATE] = self._next_date res[ATTR_WEEKS] = self._weeks_remaining if self._show_half_anniversary: res[ATTR_HALF_DATE] = self._half_date res[ATTR_HALF_DAYS] = self._half_days_remaining return res @property def icon(self): return self._icon @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" if self._state in ["Invalid Date", "Invalid Template"]: return return self._unit_of_measurement async def async_update(self): """update the sensor""" if self._template_sensor: try: template_date = templater.Template(self._date_template, self.hass).async_render() self._date, self._unknown_year = validate_date(template_date) self._date = self._date.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE) except: self._state = "Invalid Template" return if self._date == "Invalid Date": self._state = self._date return today = date.today() years = today.year - self._date.year nextDate = self._date.date() if today >= self._date.date() + relativedelta(year=today.year): years = years + 1 if not self._one_time: if today >= nextDate: nextDate = self._date.date() + relativedelta(year=today.year) if today > nextDate: nextDate = self._date.date() + relativedelta(year=today.year + 1) self._next_date = datetime.combine(nextDate, datetime.min.time()) self._next_date = self._next_date.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE) daysRemaining = (nextDate - today).days if self._unknown_year: self._date = datetime(nextDate.year, nextDate.month, nextDate.day) self._date = self._date.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE) if daysRemaining == 0: self._icon = self._icon_today elif daysRemaining <= self._soon: self._icon = self._icon_soon else: self._icon = self._icon_normal self._state = daysRemaining if daysRemaining == 0: self._years_next = years - 1 else: self._years_next = years self._years_current = years - 1 self._weeks_remaining = int(daysRemaining / 7) if self._count_up: if daysRemaining > 0 and not self._one_time: nextDate = nextDate + relativedelta(years=-1) self._state = (today - nextDate).days if self._show_half_anniversary: nextHalfDate = self._half_date.date() if today > nextHalfDate: nextHalfDate = self._half_date.date() + relativedelta(year = today.year) if today > nextHalfDate: nextHalfDate = self._half_date.date() + relativedelta(year = today.year + 1) self._half_days_remaining = (nextHalfDate - today).days self._half_date = datetime(nextHalfDate.year, nextHalfDate.month, nextHalfDate.day) self._half_date = self._half_date.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE) async def async_added_to_hass(self): """Once the entity is added we should update to get the initial data loaded. Then add it to the Calendar.""" await super().async_added_to_hass() self.async_schedule_update_ha_state(True) if DOMAIN not in self.hass.data: self.hass.data[DOMAIN] = {} if SENSOR_PLATFORM not in self.hass.data[DOMAIN]: self.hass.data[DOMAIN][SENSOR_PLATFORM] = {} self.hass.data[DOMAIN][SENSOR_PLATFORM][self.entity_id] = self if CALENDAR_PLATFORM not in self.hass.data[DOMAIN]: self.hass.data[DOMAIN][ CALENDAR_PLATFORM ] = EntitiesCalendarData(self.hass) _LOGGER.debug("Creating Anniversaries calendar") self.hass.async_create_task( async_load_platform( self.hass, CALENDAR_PLATFORM, DOMAIN, {"name": CALENDAR_NAME}, {"name": CALENDAR_NAME}, ) ) else: _LOGGER.debug("Anniversaries calendar already exists") self.hass.data[DOMAIN][CALENDAR_PLATFORM].add_entity(self.entity_id) async def async_will_remove_from_hass(self): """When sensor is removed from hassio and there are no other sensors in the Anniversaries calendar, remove it.""" await super().async_will_remove_from_hass() _LOGGER.debug("Removing: %s" % (self._name)) del self.hass.data[DOMAIN][SENSOR_PLATFORM][self.entity_id] self.hass.data[DOMAIN][CALENDAR_PLATFORM].remove_entity(self.entity_id)