Skip to content

Commit

Permalink
Merge pull request #83 from tykeal/link_to_keymaster
Browse files Browse the repository at this point in the history
Feat: Guest identifiers and lock codes
  • Loading branch information
tykeal authored Mar 21, 2022
2 parents b43c679 + be858f8 commit d11baf7
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 9 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`
4 changes: 4 additions & 0 deletions custom_components/rental_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions custom_components/rental_control/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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]],
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions custom_components/rental_control/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -34,13 +35,20 @@
# Defaults
DEFAULT_CHECKIN = "16:00"
DEFAULT_CHECKOUT = "11:00"
DEFAULT_CODE_GENERATION = "date_based"
DEFAULT_DAYS = 365
DEFAULT_EVENT_PREFIX = ""
DEFAULT_MAX_EVENTS = 5
DEFAULT_NAME = DOMAIN
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}
Expand Down
100 changes: 99 additions & 1 deletion custom_components/rental_control/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Creating sensors for upcoming events."""
import logging
import random
import re
from datetime import datetime
from datetime import timedelta

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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(
Expand All @@ -162,5 +258,7 @@ async def async_update(self):
"start": None,
"end": None,
"eta": None,
"slot_name": None,
"slot_code": None,
}
self._state = summary
2 changes: 2 additions & 0 deletions custom_components/rental_control/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions custom_components/rental_control/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
21 changes: 17 additions & 4 deletions info.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`

0 comments on commit d11baf7

Please sign in to comment.