home_assistant/custom_components/anniversaries/sensor.py

248 lines
9.3 KiB
Python

""" 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)