initial commit

This commit is contained in:
2022-12-20 21:26:47 +01:00
commit 2962a6db69
722 changed files with 63886 additions and 0 deletions

View File

View File

@@ -0,0 +1,88 @@
import logging
from datetime import timedelta
SENSOR_TYPES = {
"gft": ["GFT", "mdi:recycle"],
"kerstboom": ["Kerstboom", "mdi:recycle"],
"papier": ["Papier", "mdi:recycle"],
"pbd": ["PBD", "mdi:recycle"],
"restafval": ["Restafval", "mdi:recycle"],
"takken": ["Takken", "mdi:recycle"],
"textiel": ["Textiel", "mdi:recycle"],
"trash_type_today": ["Today", "mdi:recycle"],
"trash_type_tomorrow": ["Tomorrow", "mdi:recycle"],
}
SENSOR_LOCATIONS_TO_URL = {
"trashapi": [
"http://trashapi.azurewebsites.net/trash?Location={0}&ZipCode={1}&HouseNumber={2}&HouseNumberSuffix={3}&DiftarCode={4}"
]
}
MONTH_TO_NUMBER = {
"jan": "01",
"feb": "02",
"mrt": "03",
"apr": "04",
"mei": "05",
"jun": "06",
"jul": "07",
"aug": "08",
"sep": "09",
"okt": "10",
"nov": "11",
"dec": "12",
"januari": "01",
"februari": "02",
"maart": "03",
"april": "04",
"mei": "05",
"juni": "06",
"juli": "07",
"augustus": "08",
"september": "09",
"oktober": "10",
"november": "11",
"december": "12",
}
NUMBER_TO_MONTH = {
1: "januari",
2: "februari",
3: "maart",
4: "april",
5: "mei",
6: "juni",
7: "juli",
8: "augustus",
9: "september",
10: "oktober",
11: "november",
12: "december",
}
CONF_CITY = "city"
CONF_LOCATION = "location"
CONF_POSTCODE = "postcode"
CONF_STREET_NUMBER = "streetnumber"
CONF_STREET_NUMBER_SUFFIX = "streetnumbersuffix"
CONF_DATE_FORMAT = "dateformat"
CONF_TIMESPAN_IN_DAYS = "timespanindays"
CONF_LOCALE = "locale"
CONF_ID = "id"
CONF_NO_TRASH_TEXT = "notrashtext"
CONF_DIFTAR_CODE = "diftarcode"
SENSOR_PREFIX = "Afvalinfo "
ATTR_ERROR = "error"
ATTR_LAST_UPDATE = "last_update"
ATTR_HIDDEN = "hidden"
ATTR_IS_COLLECTION_DATE_TODAY = "is_collection_date_today"
ATTR_DAYS_UNTIL_COLLECTION_DATE = "days_until_collection_date"
ATTR_YEAR_MONTH_DAY_DATE = "year_month_day_date"
ATTR_FRIENDLY_NAME = "friendly_name"
ATTR_LAST_COLLECTION_DATE = "last_collection_date"
ATTR_TOTAL_COLLECTIONS_THIS_YEAR = "total_collections_this_year"
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2, minutes=30)

View File

@@ -0,0 +1,82 @@
from ..const.const import (
MONTH_TO_NUMBER,
SENSOR_LOCATIONS_TO_URL,
_LOGGER,
)
from datetime import date, datetime, timedelta
import urllib.request
import urllib.error
import requests
class TrashApiAfval(object):
def get_data(
self,
location,
postcode,
street_number,
street_number_suffix,
diftar_code,
resources,
):
_LOGGER.debug("Updating Waste collection dates")
try:
API_ENDPOINT = SENSOR_LOCATIONS_TO_URL["trashapi"][0].format(
location, postcode, street_number, street_number_suffix, diftar_code
)
r = requests.get(url=API_ENDPOINT)
dataList = r.json()
# Place all possible values in the dictionary even if they are not necessary
waste_dict = {}
# _LOGGER.warning(dataList)
for data in dataList:
# find gft.
if "gft" in resources and data["name"].lower() == "gft":
waste_dict["gft"] = data["date"].split("T")[0]
# find kerstboom.
if "kerstboom" in resources and data["name"].lower() == "kerstboom":
waste_dict["kerstboom"] = data["date"].split("T")[0]
# find papier
if "papier" in resources and data["name"].lower() == "papier":
waste_dict["papier"] = data["date"].split("T")[0]
# find pbd.
if "pbd" in resources and data["name"].lower() == "pbd":
waste_dict["pbd"] = data["date"].split("T")[0]
# find restafval.
if "restafval" in resources and data["name"].lower() == "restafval":
if (
date.today()
<= datetime.strptime(
data["date"].split("T")[0], "%Y-%m-%d"
).date()
):
waste_dict["restafval"] = data["date"].split("T")[0]
else:
waste_dict["restafvaldiftardate"] = data["date"].split("T")[0]
waste_dict["restafvaldiftarcollections"] = data["totalThisYear"]
# find takken
if "takken" in resources and data["name"].lower() == "takken":
waste_dict["takken"] = data["date"].split("T")[0]
# find textiel
if "textiel" in resources and data["name"].lower() == "textiel":
waste_dict["textiel"] = data["date"].split("T")[0]
return waste_dict
except urllib.error.URLError as exc:
_LOGGER.error("Error occurred while fetching data: %r", exc.reason)
return False
except Exception as exc:
_LOGGER.error(
"""Error occurred. Please check the address with postcode: %r and huisnummer: %r%r on the website of your local waste collector in the gemeente: %r. It's probably a faulty address or the website of the waste collector is unreachable. If the address is working on the website of the local waste collector and this error still occured, please report the issue in the Github repository https://github.com/heyajohnny/afvalinfo with details of the location that isn't working""",
postcode,
street_number,
street_number_suffix,
location,
)
return False

View File

@@ -0,0 +1,15 @@
{
"domain": "afvalinfo",
"name": "Afvalinfo",
"version": "1.0.9",
"documentation": "https://github.com/heyajohnny/afvalinfo",
"issue_tracker": "https://github.com/heyajohnny/afvalinfo/issues",
"dependencies": [],
"codeowners": [
"@heyajohnny"
],
"requirements": [
"Babel==2.8.0",
"python-dateutil==2.8.1"
]
}

View File

@@ -0,0 +1,372 @@
#!/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_LOCATION,
CONF_POSTCODE,
CONF_STREET_NUMBER,
CONF_STREET_NUMBER_SUFFIX,
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,
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_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,
}
)
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)
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)
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,
diftar_code,
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,
)
)
# 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,
diftar_code,
resources,
):
self.data = None
self.location = location
self.postcode = postcode
self.street_number = street_number
self.street_number_suffix = street_number_suffix
self.diftar_code = diftar_code
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.diftar_code,
self.resources,
)
class AfvalinfoSensor(Entity):
def __init__(
self,
data,
sensor_type,
sensor_friendly_name,
date_format,
timespan_in_days,
locale,
id_name,
):
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.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
@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,
}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
self.data.update()
waste_data = self.data.data
self._error = False
try:
if waste_data:
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
else:
self._hidden = True
else:
raise ValueError()
else:
raise ValueError()
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._last_update = datetime.today().strftime("%d-%m-%Y %H:%M")

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
from datetime import datetime, date, timedelta
from .const.const import (
_LOGGER,
ATTR_LAST_UPDATE,
ATTR_FRIENDLY_NAME,
ATTR_YEAR_MONTH_DAY_DATE,
SENSOR_TYPES,
SENSOR_PREFIX,
)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
class AfvalInfoTodaySensor(Entity):
def __init__(
self, data, sensor_type, sensor_friendly_name, entities, id_name, no_trash_text
):
self.data = data
self.type = sensor_type
self.friendly_name = sensor_friendly_name
self._last_update = None
self._name = sensor_friendly_name
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._no_trash_text = no_trash_text
self._state = None
self._icon = SENSOR_TYPES[sensor_type][1]
self._entities = entities
@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_LAST_UPDATE: self._last_update}
@Throttle(timedelta(minutes=1))
def update(self):
self.data.update()
self._last_update = datetime.today().strftime("%d-%m-%Y %H:%M")
# use a tempState to change the real state only on a change...
tempState = self._no_trash_text
numberOfMatches = 0
today = str(date.today().strftime("%Y-%m-%d"))
for entity in self._entities:
if entity.extra_state_attributes.get(ATTR_YEAR_MONTH_DAY_DATE) == today:
# reset tempState to empty string
if numberOfMatches == 0:
tempState = ""
numberOfMatches = numberOfMatches + 1
# add trash friendly name or if no friendly name is provided, trash type to string
tempState = (
(
tempState
+ ", "
+ entity.extra_state_attributes.get(ATTR_FRIENDLY_NAME)
)
).strip()
if tempState.startswith(", "):
tempState = tempState[2:]
# only change state if the new state is different than the last state
if tempState != self._state:
self._state = tempState

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
from datetime import datetime, date, timedelta
from .const.const import (
_LOGGER,
ATTR_LAST_UPDATE,
ATTR_FRIENDLY_NAME,
ATTR_YEAR_MONTH_DAY_DATE,
SENSOR_TYPES,
SENSOR_PREFIX,
)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
class AfvalInfoTomorrowSensor(Entity):
def __init__(
self, data, sensor_type, sensor_friendly_name, entities, id_name, no_trash_text
):
self.data = data
self.type = sensor_type
self.friendly_name = sensor_friendly_name
self._last_update = None
self._name = sensor_friendly_name
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._no_trash_text = no_trash_text
self._state = None
self._icon = SENSOR_TYPES[sensor_type][1]
self._entities = entities
@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_LAST_UPDATE: self._last_update}
@Throttle(timedelta(minutes=1))
def update(self):
self.data.update()
self._last_update = datetime.today().strftime("%d-%m-%Y %H:%M")
# use a tempState to change the real state only on a change...
tempState = self._no_trash_text
numberOfMatches = 0
tomorrow = str((date.today() + timedelta(days=1)).strftime("%Y-%m-%d"))
for entity in self._entities:
if entity.extra_state_attributes.get(ATTR_YEAR_MONTH_DAY_DATE) == tomorrow:
# reset tempState to empty string
if numberOfMatches == 0:
tempState = ""
numberOfMatches = numberOfMatches + 1
# add trash name to string
tempState = (
(
tempState
+ ", "
+ entity.extra_state_attributes.get(ATTR_FRIENDLY_NAME)
)
).strip()
if tempState.startswith(", "):
tempState = tempState[2:]
# only change state if the new state is different than the last state
if tempState != self._state:
self._state = tempState