From 8e0bbd75fb92e20c7f1fc5320af3fbc3321967f3 Mon Sep 17 00:00:00 2001 From: Andrew Grimberg Date: Sun, 20 Mar 2022 13:21:39 -0700 Subject: [PATCH 1/4] Docs: Document code generator options Issue: #38 Signed-off-by: Andrew Grimberg --- README.md | 21 +++++++++++++++++---- info.md | 21 +++++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 00109b8..3fb0664 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,19 @@ calendars and sensors to go with them related to managing rental properties. entities if more than one calendar is being tracked in an instance - Forcing a calendar refresh is currently possible by submitting a configuration change +- 3 door code generators are available: + - A check-in/out date based 4 digit code using the check-in day combined + with the check-out day (default and fallback in the case another + generator fails to produce a code) + - A random 4 digit code based on the event description + - The last 4 digits of the phone number. This only works properly if the + event description contains 'Last 4 Digits' followed quickly by a 4 digit + number. This is the most stable, but only works if the event + descriptions have the needed data. The previous two methods can have the + codes change if the event makes changes to length or to the description. +- All events will get a code associated with it. In the case that the criteria + to create the code are not fulfilled, then the check-in/out date based + method will be used as a fallback ## Planned features @@ -92,7 +105,7 @@ This integration supports reconfiguration after initial setup - Select the calendar and then select `Configure` - Reconfigure as if you were setting it up for the first time -**NOTE:** Changes may not be picked up right away. The update cycle of the -calendar is to check for updates every 2 minutes and events are refreshed around -every 30 seconds. If you want to force a full update right away, select the -`...` menu next to `Configure` and select `Reload` +**NOTE:** Changes may not be picked up right away. The default update cycle of +the calendar is to check for updates every 2 minutes and events are refreshed +around every 30 seconds. If you want to force a full update right away, select +the `...` menu next to `Configure` and select `Reload` diff --git a/info.md b/info.md index 48b2670..d8e3246 100644 --- a/info.md +++ b/info.md @@ -32,6 +32,19 @@ calendars and sensors to go with them related to managing rental properties. entities if more than one calendar is being tracked in an instance - Forcing a calendar refresh is currently possible by submitting a configuration change +- 3 door code generators are available: + - A check-in/out date based 4 digit code using the check-in day combined + with the check-out day (default and fallback in the case another + generator fails to produce a code) + - A random 4 digit code based on the event description + - The last 4 digits of the phone number. This only works properly if the + event description contains 'Last 4 Digits' followed quickly by a 4 digit + number. This is the most stable, but only works if the event + descriptions have the needed data. The previous two methods can have the + codes change if the event makes changes to length or to the description. +- All events will get a code associated with it. In the case that the criteria + to create the code are not fulfilled, then the check-in/out date based + method will be used as a fallback ## Planned features @@ -67,7 +80,7 @@ This integration supports reconfiguration after initial setup - Select the calendar and then select `Configure` - Reconfigure as if you were setting it up for the first time -**NOTE:** Changes may not be picked up right away. The update cycle of the -calendar is to check for updates every 2 minutes and events are refreshed around -every 30 seconds. If you want to force a full update right away, select the -`...` menu next to `Configure` and select `Reload` +**NOTE:** Changes may not be picked up right away. The default update cycle of +the calendar is to check for updates every 2 minutes and events are refreshed +around every 30 seconds. If you want to force a full update right away, select +the `...` menu next to `Configure` and select `Reload` From 7a7d9f32cf44cdd5b4c579089a99fa77ab36d17d Mon Sep 17 00:00:00 2001 From: Andrew Grimberg Date: Sat, 19 Mar 2022 18:20:19 -0700 Subject: [PATCH 2/4] Feat: Add code generator options to config flow Add the code generator options to the config flow. This currently does nothing and is not used by anything else yet, but it's the first step in making code generators possible. Issue: #38 Signed-off-by: Andrew Grimberg --- .../rental_control/config_flow.py | 39 +++++++++++++++++++ custom_components/rental_control/const.py | 8 ++++ custom_components/rental_control/strings.json | 2 + .../rental_control/translations/en.json | 2 + 4 files changed, 51 insertions(+) diff --git a/custom_components/rental_control/config_flow.py b/custom_components/rental_control/config_flow.py index 05a2291..081f4a7 100644 --- a/custom_components/rental_control/config_flow.py +++ b/custom_components/rental_control/config_flow.py @@ -21,8 +21,10 @@ from pytz import common_timezones from voluptuous.schema_builder import ALLOW_EXTRA +from .const import CODE_GENERATORS 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 @@ -33,6 +35,7 @@ from .const import CONF_TIMEZONE from .const import DEFAULT_CHECKIN from .const import DEFAULT_CHECKOUT +from .const import DEFAULT_CODE_GENERATION from .const import DEFAULT_DAYS from .const import DEFAULT_EVENT_PREFIX from .const import DEFAULT_MAX_EVENTS @@ -141,6 +144,30 @@ def _available_lock_managers( return data +def _code_generators() -> list: + """Return list of code genrators available.""" + + data = [] + + for generator in CODE_GENERATORS: + data.append(generator["description"]) + + return data + + +def _generator_convert(ident: str, to_type: bool = True) -> str: + """Convert between type and description for generators.""" + + if to_type: + return next(item for item in CODE_GENERATORS if item["description"] == ident)[ + "type" + ] + else: + return next(item for item in CODE_GENERATORS if item["type"] == ident)[ + "description" + ] + + def _get_schema( hass: HomeAssistant, user_input: Optional[Dict[str, Any]], @@ -196,6 +223,13 @@ def _get_default(key: str, fallback_default: Any = None) -> None: CONF_MAX_EVENTS, default=_get_default(CONF_MAX_EVENTS, DEFAULT_MAX_EVENTS), ): cv.positive_int, + vol.Optional( + CONF_CODE_GENERATION, + default=_generator_convert( + ident=_get_default(CONF_CODE_GENERATION, DEFAULT_CODE_GENERATION), + to_type=False, + ), + ): vol.In(_code_generators()), vol.Optional( CONF_IGNORE_NON_RESERVED, default=_get_default(CONF_IGNORE_NON_RESERVED, True), @@ -292,6 +326,11 @@ async def _start_config_flow( if user_input[CONF_LOCK_ENTRY] == "(none)": user_input[CONF_LOCK_ENTRY] = None + # Convert code generator to proper type + user_input[CONF_CODE_GENERATION] = _generator_convert( + ident=user_input[CONF_CODE_GENERATION], to_type=True + ) + return cls.async_create_entry(title=title, data=user_input) return _show_config_form( diff --git a/custom_components/rental_control/const.py b/custom_components/rental_control/const.py index e86091e..9877da7 100644 --- a/custom_components/rental_control/const.py +++ b/custom_components/rental_control/const.py @@ -22,6 +22,7 @@ # Config CONF_CHECKIN = "checkin" CONF_CHECKOUT = "checkout" +CONF_CODE_GENERATION = "code_generation" CONF_DAYS = "days" CONF_EVENT_PREFIX = "event_prefix" CONF_IGNORE_NON_RESERVED = "ignore_non_reserved" @@ -34,6 +35,7 @@ # Defaults DEFAULT_CHECKIN = "16:00" DEFAULT_CHECKOUT = "11:00" +DEFAULT_CODE_GENERATION = "date_based" DEFAULT_DAYS = 365 DEFAULT_EVENT_PREFIX = "" DEFAULT_MAX_EVENTS = 5 @@ -41,6 +43,12 @@ DEFAULT_REFRESH_FREQUENCY = 2 DEFAULT_START_SLOT = 10 +CODE_GENERATORS = [ + {"type": "date_based", "description": "Start/End Date"}, + {"type": "static_random", "description": "Static Random"}, + {"type": "last_four", "description": "Last 4 Phone Digits"}, +] + STARTUP_MESSAGE = f""" ------------------------------------------------------------------- {NAME} diff --git a/custom_components/rental_control/strings.json b/custom_components/rental_control/strings.json index f7fe207..3da968c 100644 --- a/custom_components/rental_control/strings.json +++ b/custom_components/rental_control/strings.json @@ -13,6 +13,7 @@ "data": { "checkin": "Default Check-in time", "checkout": "Default Check-out time", + "code_generation": "Door code generator", "days": "Maximum number of days into the future to fetch", "event_prefix": "Prefix for all events (useful if linking more than one rental)", "ignore_non_reserved": "Ignore events that are not standard reservations", @@ -42,6 +43,7 @@ "data": { "checkin": "Default Check-in time", "checkout": "Default Check-out time", + "code_generation": "Door code generator", "days": "Maximum number of days into the future to fetch", "event_prefix": "Prefix for all events (useful if linking more than one rental)", "ignore_non_reserved": "Ignore events that are not standard reservations", diff --git a/custom_components/rental_control/translations/en.json b/custom_components/rental_control/translations/en.json index bae6014..f8e5de7 100644 --- a/custom_components/rental_control/translations/en.json +++ b/custom_components/rental_control/translations/en.json @@ -13,6 +13,7 @@ "data": { "checkin": "Default Check-in time", "checkout": "Default Check-out time", + "code_generation": "Door code generator", "days": "Maximum number of days into the future to fetch", "event_prefix": "Prefix for all events (useful if linking more than one rental)", "ignore_non_reserved": "Ignore events that are not standard reservations", @@ -42,6 +43,7 @@ "data": { "checkin": "Default Check-in time", "checkout": "Default Check-out time", + "code_generation": "Door code generator", "days": "Maximum number of days into the future to fetch", "event_prefix": "Prefix for all events (useful if linking more than one rental)", "ignore_non_reserved": "Ignore events that are not standard reservations", From bbdf60169940577cea59c9c776d35afd1621c0a2 Mon Sep 17 00:00:00 2001 From: Andrew Grimberg Date: Sun, 20 Mar 2022 14:51:10 -0700 Subject: [PATCH 3/4] 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 From be858f885b1eafb2d73af64bd8f24b6faceef0b6 Mon Sep 17 00:00:00 2001 From: Andrew Grimberg Date: Sun, 20 Mar 2022 17:00:10 -0700 Subject: [PATCH 4/4] Feat: Capture guest name Teach the sensor how to determine a unique (short) name for each guest Signed-off-by: Andrew Grimberg --- custom_components/rental_control/sensor.py | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/custom_components/rental_control/sensor.py b/custom_components/rental_control/sensor.py index 2573d97..bb967a8 100644 --- a/custom_components/rental_control/sensor.py +++ b/custom_components/rental_control/sensor.py @@ -84,6 +84,7 @@ def __init__(self, hass, rental_control_events, sensor_name, event_number): "start": None, "end": None, "eta": None, + "slot_name": None, "slot_code": None, } self._state = summary @@ -139,6 +140,40 @@ def _generate_door_code(self) -> str: else: return ret + def _get_slot_name(self) -> str: + """Determine the name for a door slot.""" + + # strip off any prefix if it's being used + if self.rental_control_events.event_prefix: + p = re.compile(f"{self.rental_control_events.event_prefix} (.*)") + summary = p.findall(self._event_attributes["summary"])[0] + else: + summary = self._event_attributes["summary"] + + # Blocked and Unavailable should not have a slot + p = re.compile("Not available|Blocked") + if p.search(summary): + return None + + # AirBnB & VRBO + if re.search("Reserved", summary): + # AirBnB + if summary == "Reserved": + p = re.compile("([A-Z][A-Z0-9]{9})") + return p.search(self._event_attributes["description"])[0] + else: + p = re.compile(" - (.*)$") + return p.findall(summary)[0] + + # Tripadvisor + if re.search("Tripadvisor", summary): + p = re.compile("Tripadvisor.*: (.*)") + return p.findall(summary)[0] + + # Guesty + p = re.compile("-(.*)-.*-") + return p.findall(summary)[0] + @property def entity_id(self): """Return the entity_id of the sensor.""" @@ -203,6 +238,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_name"] = self._get_slot_name() self._event_attributes["slot_code"] = self._generate_door_code() else: # No reservations @@ -222,6 +258,7 @@ async def async_update(self): "start": None, "end": None, "eta": None, + "slot_name": None, "slot_code": None, } self._state = summary