From bbdf60169940577cea59c9c776d35afd1621c0a2 Mon Sep 17 00:00:00 2001 From: Andrew Grimberg Date: Sun, 20 Mar 2022 14:51:10 -0700 Subject: [PATCH] Feat: Add code generators Teach the event sensor 3 ways to generate door codes. The default (and fall back) is check-in/out day based where the check-in day and check-out day are combined to create a 4 digit code A random 4 digit code seeded from the event description Finally, the last 4 digits of a phone number - only works if the description has the text 'Last 4 Digits' followed quickly by a 4 digit number. This is by far the most stable, but only works if the conditions are correct Issue: #38 Signed-off-by: Andrew Grimberg --- custom_components/rental_control/__init__.py | 4 ++ custom_components/rental_control/sensor.py | 63 +++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/custom_components/rental_control/__init__.py b/custom_components/rental_control/__init__.py index 92628b8..0af2afe 100644 --- a/custom_components/rental_control/__init__.py +++ b/custom_components/rental_control/__init__.py @@ -31,12 +31,14 @@ from .const import CONF_CHECKIN from .const import CONF_CHECKOUT +from .const import CONF_CODE_GENERATION from .const import CONF_DAYS from .const import CONF_EVENT_PREFIX from .const import CONF_IGNORE_NON_RESERVED from .const import CONF_MAX_EVENTS from .const import CONF_REFRESH_FREQUENCY from .const import CONF_TIMEZONE +from .const import DEFAULT_CODE_GENERATION from .const import DEFAULT_REFRESH_FREQUENCY from .const import DOMAIN from .const import PLATFORMS @@ -146,6 +148,7 @@ def __init__(self, hass, config): self.ignore_non_reserved = config.get(CONF_IGNORE_NON_RESERVED) self.verify_ssl = config.get(CONF_VERIFY_SSL) self.calendar = [] + self.code_generator = config.get(CONF_CODE_GENERATION, DEFAULT_CODE_GENERATION) self.event = None self.all_day = False @@ -214,6 +217,7 @@ def update_config(self, config): self.checkout = cv.time(config.get(CONF_CHECKOUT)) self.max_events = config.get(CONF_MAX_EVENTS) self.days = config.get(CONF_DAYS) + self.code_generator = config.get(CONF_CODE_GENERATION, DEFAULT_CODE_GENERATION) # Early versions did not have this variable, as such it may not be # set, this should guard against issues until we're certain # we can remove this guard. diff --git a/custom_components/rental_control/sensor.py b/custom_components/rental_control/sensor.py index 2f2b886..2573d97 100644 --- a/custom_components/rental_control/sensor.py +++ b/custom_components/rental_control/sensor.py @@ -1,5 +1,7 @@ """Creating sensors for upcoming events.""" import logging +import random +import re from datetime import datetime from datetime import timedelta @@ -36,7 +38,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] for eventnumber in range(max_events): sensors.append( - ICalSensor(hass, rental_control_events, DOMAIN + " " + name, eventnumber) + ICalSensor( + hass, + rental_control_events, + DOMAIN + " " + name, + eventnumber, + ) ) async_add_entities(sensors) @@ -77,9 +84,60 @@ def __init__(self, hass, rental_control_events, sensor_name, event_number): "start": None, "end": None, "eta": None, + "slot_code": None, } self._state = summary self._is_available = None + self._code_generator = rental_control_events.code_generator + + def _generate_door_code(self) -> str: + """Generate a door code based upon the selected type.""" + + generator = self._code_generator + + # If there is no event description force date_based generation + # This is because VRBO does not appear to provide any descriptions in + # their calendar entries! + # This also gets around Unavailable and Blocked entries that do not + # have a description either + if self._event_attributes["description"] is None: + generator = "date_based" + + # AirBnB provides the last 4 digits of the guest's registered phone + # + # VRBO does not appear to provide any phone numbers + # + # Guesty provides last 4 + either a full number or all but last digit + # for VRBO listings and doesn't appear to provide anything for AirBnB + # listings, or if it does provide them, my example Guesty calendar doesn't + # have any new enough to have the data + # + # TripAdvisor does not appear to provide any phone number data + + ret = None + + if generator == "last_four": + p = re.compile("\\(Last 4 Digits\\):\\s+(\\d{4})") + last_four = p.findall(self._event_attributes["description"])[0] + ret = last_four + elif generator == "static_random": + # If the description changes this will most likely change the code + random.seed(self._event_attributes["description"]) + ret = str(random.randrange(1, 9999, 4)).zfill(4) + + if ret is None: + # Generate code based on checkin/out days + # + # This generator will have a side effect of changing the code + # if the start or end dates shift! + # + # This is the default and fall back generator if no other + # generator produced a code + start_day = self._event_attributes["start"].strftime("%d") + end_day = self._event_attributes["end"].strftime("%d") + return f"{start_day}{end_day}" + else: + return ret @property def entity_id(self): @@ -117,6 +175,7 @@ async def async_update(self): await self.rental_control_events.update() + self._code_generator = self.rental_control_events.code_generator event_list = self.rental_control_events.calendar if event_list and (self._event_number < len(event_list)): val = event_list[self._event_number] @@ -144,6 +203,7 @@ async def async_update(self): self._state = f"{name} - {start.strftime('%-d %B %Y')}" if not val.get("all_day"): self._state += f" {start.strftime('%H:%M')}" + self._event_attributes["slot_code"] = self._generate_door_code() else: # No reservations _LOGGER.debug( @@ -162,5 +222,6 @@ async def async_update(self): "start": None, "end": None, "eta": None, + "slot_code": None, } self._state = summary