414 lines
15 KiB
Python
414 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Sensor component for Afvalinfo
|
|
Author: Johnny Visser
|
|
"""
|
|
|
|
import voluptuous as vol
|
|
from datetime import datetime, date, timedelta
|
|
from dateutil.relativedelta import relativedelta
|
|
import urllib.error
|
|
from babel import Locale
|
|
from babel.dates import format_date, format_datetime, format_time
|
|
import re
|
|
|
|
from .const.const import (
|
|
MIN_TIME_BETWEEN_UPDATES,
|
|
_LOGGER,
|
|
CONF_CITY,
|
|
CONF_DISTRICT,
|
|
CONF_LOCATION,
|
|
CONF_POSTCODE,
|
|
CONF_STREET_NUMBER,
|
|
CONF_STREET_NUMBER_SUFFIX,
|
|
CONF_GET_WHOLE_YEAR,
|
|
CONF_DATE_FORMAT,
|
|
CONF_TIMESPAN_IN_DAYS,
|
|
CONF_NO_TRASH_TEXT,
|
|
CONF_DIFTAR_CODE,
|
|
CONF_LOCALE,
|
|
CONF_ID,
|
|
SENSOR_PREFIX,
|
|
ATTR_ERROR,
|
|
ATTR_LAST_UPDATE,
|
|
ATTR_HIDDEN,
|
|
ATTR_DAYS_UNTIL_COLLECTION_DATE,
|
|
ATTR_IS_COLLECTION_DATE_TODAY,
|
|
ATTR_YEAR_MONTH_DAY_DATE,
|
|
ATTR_FRIENDLY_NAME,
|
|
ATTR_LAST_COLLECTION_DATE,
|
|
ATTR_TOTAL_COLLECTIONS_THIS_YEAR,
|
|
ATTR_WHOLE_YEAR_DATES,
|
|
SENSOR_TYPES,
|
|
)
|
|
|
|
from .location.trashapi import TrashApiAfval
|
|
from .sensortomorrow import AfvalInfoTomorrowSensor
|
|
from .sensortoday import AfvalInfoTodaySensor
|
|
|
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.const import CONF_RESOURCES
|
|
from homeassistant.util import Throttle
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
{
|
|
vol.Required(CONF_RESOURCES, default=[]): vol.All(cv.ensure_list),
|
|
vol.Optional(CONF_CITY, default=""): cv.string,
|
|
vol.Optional(CONF_LOCATION, default="sliedrecht"): cv.string,
|
|
vol.Required(CONF_POSTCODE, default="3361AB"): cv.string,
|
|
vol.Required(CONF_STREET_NUMBER, default="1"): cv.string,
|
|
vol.Optional(CONF_STREET_NUMBER_SUFFIX, default=""): cv.string,
|
|
vol.Optional(CONF_DISTRICT, default=""): cv.string,
|
|
vol.Optional(CONF_DATE_FORMAT, default="%d-%m-%Y"): cv.string,
|
|
vol.Optional(CONF_TIMESPAN_IN_DAYS, default="365"): cv.string,
|
|
vol.Optional(CONF_LOCALE, default="en"): cv.string,
|
|
vol.Optional(CONF_ID, default=""): cv.string,
|
|
vol.Optional(CONF_NO_TRASH_TEXT, default="none"): cv.string,
|
|
vol.Optional(CONF_DIFTAR_CODE, default=""): cv.string,
|
|
vol.Optional(CONF_GET_WHOLE_YEAR, default="false"): cv.string,
|
|
}
|
|
)
|
|
|
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
|
_LOGGER.debug("Setup Afvalinfo sensor")
|
|
|
|
location = config.get(CONF_CITY).lower().strip()
|
|
if len(location) == 0:
|
|
location = config.get(CONF_LOCATION).lower().strip()
|
|
postcode = config.get(CONF_POSTCODE).strip()
|
|
street_number = config.get(CONF_STREET_NUMBER)
|
|
street_number_suffix = config.get(CONF_STREET_NUMBER_SUFFIX)
|
|
district = config.get(CONF_DISTRICT)
|
|
date_format = config.get(CONF_DATE_FORMAT).strip()
|
|
timespan_in_days = config.get(CONF_TIMESPAN_IN_DAYS)
|
|
locale = config.get(CONF_LOCALE)
|
|
id_name = config.get(CONF_ID)
|
|
no_trash_text = config.get(CONF_NO_TRASH_TEXT)
|
|
diftar_code = config.get(CONF_DIFTAR_CODE)
|
|
get_whole_year = config.get(CONF_GET_WHOLE_YEAR)
|
|
|
|
try:
|
|
resources = config[CONF_RESOURCES].copy()
|
|
|
|
# filter the types from the dict if it's a dictionary
|
|
if isinstance(resources[0], dict):
|
|
resourcesMinusTodayAndTomorrow = [obj["type"] for obj in resources]
|
|
else:
|
|
resourcesMinusTodayAndTomorrow = resources
|
|
|
|
if "trash_type_today" in resourcesMinusTodayAndTomorrow:
|
|
resourcesMinusTodayAndTomorrow.remove("trash_type_today")
|
|
if "trash_type_tomorrow" in resourcesMinusTodayAndTomorrow:
|
|
resourcesMinusTodayAndTomorrow.remove("trash_type_tomorrow")
|
|
|
|
data = AfvalinfoData(
|
|
location,
|
|
postcode,
|
|
street_number,
|
|
street_number_suffix,
|
|
district,
|
|
diftar_code,
|
|
get_whole_year,
|
|
resourcesMinusTodayAndTomorrow,
|
|
)
|
|
except urllib.error.HTTPError as error:
|
|
_LOGGER.error(error.reason)
|
|
return False
|
|
|
|
entities = []
|
|
|
|
for resource in config[CONF_RESOURCES]:
|
|
# old way, before 20220204
|
|
if type(resource) == str:
|
|
sensor_type = resource.lower()
|
|
sensor_friendly_name = sensor_type
|
|
# new way
|
|
else:
|
|
sensor_type = resource["type"].lower()
|
|
if "friendly_name" in resource.keys():
|
|
sensor_friendly_name = resource["friendly_name"]
|
|
else:
|
|
# If no friendly name is provided, use the sensor_type as friendly name
|
|
sensor_friendly_name = sensor_type
|
|
|
|
# if sensor_type not in SENSOR_TYPES:
|
|
if (
|
|
sensor_type.title().lower() != "trash_type_today"
|
|
and sensor_type.title().lower() != "trash_type_tomorrow"
|
|
):
|
|
entities.append(
|
|
AfvalinfoSensor(
|
|
data,
|
|
sensor_type,
|
|
sensor_friendly_name,
|
|
date_format,
|
|
timespan_in_days,
|
|
locale,
|
|
id_name,
|
|
get_whole_year,
|
|
)
|
|
)
|
|
|
|
# Add sensor -trash_type_today
|
|
if sensor_type.title().lower() == "trash_type_today":
|
|
today = AfvalInfoTodaySensor(
|
|
data,
|
|
sensor_type,
|
|
sensor_friendly_name,
|
|
entities,
|
|
id_name,
|
|
no_trash_text,
|
|
)
|
|
entities.append(today)
|
|
# Add sensor -trash_type_tomorrow
|
|
if sensor_type.title().lower() == "trash_type_tomorrow":
|
|
tomorrow = AfvalInfoTomorrowSensor(
|
|
data,
|
|
sensor_type,
|
|
sensor_friendly_name,
|
|
entities,
|
|
id_name,
|
|
no_trash_text,
|
|
)
|
|
entities.append(tomorrow)
|
|
|
|
add_entities(entities)
|
|
|
|
|
|
class AfvalinfoData(object):
|
|
def __init__(
|
|
self,
|
|
location,
|
|
postcode,
|
|
street_number,
|
|
street_number_suffix,
|
|
district,
|
|
diftar_code,
|
|
get_whole_year,
|
|
resources,
|
|
):
|
|
self.data = None
|
|
self.location = location
|
|
self.postcode = postcode
|
|
self.street_number = street_number
|
|
self.street_number_suffix = street_number_suffix
|
|
self.district = district
|
|
self.diftar_code = diftar_code
|
|
self.get_whole_year = get_whole_year
|
|
self.resources = resources
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
def update(self):
|
|
_LOGGER.debug("Updating Waste collection dates")
|
|
self.data = TrashApiAfval().get_data(
|
|
self.location,
|
|
self.postcode,
|
|
self.street_number,
|
|
self.street_number_suffix,
|
|
self.district,
|
|
self.diftar_code,
|
|
self.get_whole_year,
|
|
self.resources,
|
|
)
|
|
|
|
|
|
class AfvalinfoSensor(Entity):
|
|
def __init__(
|
|
self,
|
|
data,
|
|
sensor_type,
|
|
sensor_friendly_name,
|
|
date_format,
|
|
timespan_in_days,
|
|
locale,
|
|
id_name,
|
|
get_whole_year,
|
|
):
|
|
self.data = data
|
|
self.type = sensor_type
|
|
self.friendly_name = sensor_friendly_name
|
|
self.date_format = date_format
|
|
self.timespan_in_days = timespan_in_days
|
|
self.locale = locale
|
|
self._name = sensor_friendly_name
|
|
self._get_whole_year = get_whole_year
|
|
self.entity_id = "sensor." + (
|
|
(
|
|
SENSOR_PREFIX
|
|
+ (id_name + " " if len(id_name) > 0 else "")
|
|
+ sensor_friendly_name
|
|
)
|
|
.lower()
|
|
.replace(" ", "_")
|
|
)
|
|
self._attr_unique_id = (
|
|
SENSOR_PREFIX
|
|
+ (id_name + " " if len(id_name) > 0 else "")
|
|
+ sensor_friendly_name
|
|
)
|
|
self._icon = SENSOR_TYPES[sensor_type][1]
|
|
self._hidden = False
|
|
self._error = False
|
|
self._state = None
|
|
self._last_update = None
|
|
self._days_until_collection_date = None
|
|
self._is_collection_date_today = False
|
|
self._year_month_day_date = None
|
|
self._last_collection_date = None
|
|
self._total_collections_this_year = None
|
|
self._whole_year_dates = None
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
@property
|
|
def icon(self):
|
|
return self._icon
|
|
|
|
@property
|
|
def state(self):
|
|
return self._state
|
|
|
|
@property
|
|
def extra_state_attributes(self):
|
|
return {
|
|
ATTR_ERROR: self._error,
|
|
ATTR_FRIENDLY_NAME: self.friendly_name,
|
|
ATTR_YEAR_MONTH_DAY_DATE: self._year_month_day_date,
|
|
ATTR_LAST_UPDATE: self._last_update,
|
|
ATTR_HIDDEN: self._hidden,
|
|
ATTR_DAYS_UNTIL_COLLECTION_DATE: self._days_until_collection_date,
|
|
ATTR_IS_COLLECTION_DATE_TODAY: self._is_collection_date_today,
|
|
ATTR_LAST_COLLECTION_DATE: self._last_collection_date,
|
|
ATTR_TOTAL_COLLECTIONS_THIS_YEAR: self._total_collections_this_year,
|
|
ATTR_WHOLE_YEAR_DATES: self._whole_year_dates,
|
|
}
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
def update(self):
|
|
self.data.update()
|
|
waste_array = self.data.data
|
|
self._error = False
|
|
|
|
# Loop through all the dates to put the dates in the whole_year_dates attribute
|
|
if self._get_whole_year == "True":
|
|
whole_year_dates = []
|
|
for waste_data in waste_array:
|
|
if self.type in waste_data:
|
|
whole_year_dates.append(
|
|
datetime.strptime(waste_data[self.type], "%Y-%m-%d").date()
|
|
)
|
|
|
|
self._whole_year_dates = whole_year_dates
|
|
|
|
try:
|
|
if waste_array:
|
|
for waste_data in waste_array:
|
|
if self.type in waste_data:
|
|
collection_date = datetime.strptime(
|
|
waste_data[self.type], "%Y-%m-%d"
|
|
).date()
|
|
|
|
# Date in date format "%Y-%m-%d"
|
|
self._year_month_day_date = str(collection_date)
|
|
|
|
if collection_date:
|
|
# Set the values of the sensor
|
|
self._last_update = datetime.today().strftime(
|
|
"%d-%m-%Y %H:%M"
|
|
)
|
|
|
|
# Is the collection date today?
|
|
self._is_collection_date_today = (
|
|
date.today() == collection_date
|
|
)
|
|
|
|
if (
|
|
self.type == "restafval"
|
|
and "restafvaldiftardate" in waste_data
|
|
):
|
|
self._last_collection_date = str(
|
|
datetime.strptime(
|
|
waste_data["restafvaldiftardate"], "%Y-%m-%d"
|
|
).date()
|
|
)
|
|
self._total_collections_this_year = waste_data[
|
|
"restafvaldiftarcollections"
|
|
]
|
|
|
|
# Days until collection date
|
|
delta = collection_date - date.today()
|
|
self._days_until_collection_date = delta.days
|
|
|
|
# Only show the value if the date is lesser than or equal to (today + timespan_in_days)
|
|
if collection_date <= date.today() + relativedelta(
|
|
days=int(self.timespan_in_days)
|
|
):
|
|
# if the date does not contain a named day or month, return the date as normal
|
|
if (
|
|
self.date_format.find("a") == -1
|
|
and self.date_format.find("A") == -1
|
|
and self.date_format.find("b") == -1
|
|
and self.date_format.find("B") == -1
|
|
):
|
|
self._state = collection_date.strftime(
|
|
self.date_format
|
|
)
|
|
# else convert the named values to the locale names
|
|
else:
|
|
edited_date_format = self.date_format.replace(
|
|
"%a", "EEE"
|
|
)
|
|
edited_date_format = edited_date_format.replace(
|
|
"%A", "EEEE"
|
|
)
|
|
edited_date_format = edited_date_format.replace(
|
|
"%b", "MMM"
|
|
)
|
|
edited_date_format = edited_date_format.replace(
|
|
"%B", "MMMM"
|
|
)
|
|
|
|
# half babel, half date string... something like EEEE 04-MMMM-2020
|
|
half_babel_half_date = collection_date.strftime(
|
|
edited_date_format
|
|
)
|
|
|
|
# replace the digits with qquoted digits 01 --> '01'
|
|
half_babel_half_date = re.sub(
|
|
r"(\d+)", r"'\1'", half_babel_half_date
|
|
)
|
|
# transform the EEE, EEEE etc... to a real locale date, with babel
|
|
locale_date = format_date(
|
|
collection_date,
|
|
half_babel_half_date,
|
|
locale=self.locale,
|
|
)
|
|
|
|
self._state = locale_date
|
|
break # we have a result, break the loop
|
|
else:
|
|
self._hidden = True
|
|
else:
|
|
# collection_date empty
|
|
raise ValueError()
|
|
# else:
|
|
# No matching result data for current waste type, no problem
|
|
else:
|
|
raise ValueError()
|
|
except ValueError:
|
|
self._error = True
|
|
# self._state = None
|
|
# self._hidden = True
|
|
# self._days_until_collection_date = None
|
|
# self._year_month_day_date = None
|
|
# self._is_collection_date_today = False
|
|
# self._last_collection_date = None
|
|
# self._total_collections_this_year = None
|
|
# self._whole_year_dates = None
|
|
self._last_update = datetime.today().strftime("%d-%m-%Y %H:%M")
|