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/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/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/sensor.py b/custom_components/rental_control/sensor.py index 2f2b886..bb967a8 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,95 @@ 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 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 + + 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): @@ -117,6 +210,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 +238,8 @@ 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 _LOGGER.debug( @@ -162,5 +258,7 @@ async def async_update(self): "start": None, "end": None, "eta": None, + "slot_name": None, + "slot_code": None, } self._state = summary 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", 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`