From 5370c8c3e4305e623b4d7903f7564bae21d4744b Mon Sep 17 00:00:00 2001 From: Andrew Grimberg Date: Tue, 22 Mar 2022 09:06:23 -0700 Subject: [PATCH 1/2] Feat: Add util library with gen_uuid function The gen_uuid function will reliably create the same UUID given particular input string to be used as a seed. Signed-off-by: Andrew Grimberg --- custom_components/rental_control/util.py | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 custom_components/rental_control/util.py diff --git a/custom_components/rental_control/util.py b/custom_components/rental_control/util.py new file mode 100644 index 0000000..598dcc0 --- /dev/null +++ b/custom_components/rental_control/util.py @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: Apache-2.0 +############################################################################## +# COPYRIGHT 2022 Andrew Grimberg +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache 2.0 License +# which accompanies this distribution, and is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Contributors: +# Andrew Grimberg - Initial implementation +############################################################################## +"""Rental Control utils.""" +import hashlib +import logging +import uuid + +from .const import NAME + +_LOGGER = logging.getLogger(__name__) + + +def gen_uuid(created: str) -> str: + """Generation a UUID from the NAME and creation time.""" + m = hashlib.md5(f"{NAME} {created}".encode("utf-8")) + return str(uuid.UUID(m.hexdigest())) From 44079b8d6014243fb7dbf89c7254208be73082d5 Mon Sep 17 00:00:00 2001 From: Andrew Grimberg Date: Tue, 22 Mar 2022 13:15:47 -0700 Subject: [PATCH 2/2] Fix: Generate true Unique ID Using the name as the unique ID for integration configuration had issues in that it made it impossible to rename the calendar / configuration without removing and re-adding it Issue: #66 Signed-off-by: Andrew Grimberg --- custom_components/rental_control/__init__.py | 39 +++++++++++++++++-- custom_components/rental_control/calendar.py | 2 +- .../rental_control/config_flow.py | 38 ++++++++---------- custom_components/rental_control/const.py | 1 + custom_components/rental_control/sensor.py | 2 +- 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/custom_components/rental_control/__init__.py b/custom_components/rental_control/__init__.py index 0af2afe..760cf84 100644 --- a/custom_components/rental_control/__init__.py +++ b/custom_components/rental_control/__init__.py @@ -32,6 +32,7 @@ from .const import CONF_CHECKIN from .const import CONF_CHECKOUT from .const import CONF_CODE_GENERATION +from .const import CONF_CREATION_DATETIME from .const import CONF_DAYS from .const import CONF_EVENT_PREFIX from .const import CONF_IGNORE_NON_RESERVED @@ -43,6 +44,7 @@ from .const import DOMAIN from .const import PLATFORMS from .const import REQUEST_TIMEOUT +from .util import gen_uuid _LOGGER = logging.getLogger(__name__) @@ -67,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # hass.data[DOMAIN][entry.entry_id] = MyApi(...) if DOMAIN not in hass.data: hass.data[DOMAIN] = {} - hass.data[DOMAIN][config.get(CONF_NAME)] = ICalEvents(hass=hass, config=config) + hass.data[DOMAIN][entry.unique_id] = ICalEvents(hass=hass, config=config) for component in PLATFORMS: hass.async_create_task( @@ -92,11 +94,33 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) if unload_ok: - hass.data[DOMAIN].pop(config.get(CONF_NAME)) + hass.data[DOMAIN].pop(entry.unique_id) return unload_ok +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate configuration.""" + + version = config_entry.version + + # 1 -> 2: Migrate keys + if version == 1: + _LOGGER.debug("Migrating from version %s", version) + data = config_entry.data.copy() + + data[CONF_CREATION_DATETIME] = str(dt.now()) + hass.config_entries.async_update_entry( + entry=config_entry, + unique_id=gen_uuid(data[CONF_CREATION_DATETIME]), + data=data, + ) + config_entry.version = 2 + _LOGGER.debug("Migration of to version %s complete", config_entry.version) + + return True + + async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener.""" # No need to update if the options match the data @@ -105,15 +129,21 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: new_data = entry.options.copy() + old_data = hass.data[DOMAIN][entry.unique_id] + + # do not update the creation datetime if it already exists (which it should) + new_data[CONF_CREATION_DATETIME] = old_data.created + hass.config_entries.async_update_entry( entry=entry, - unique_id=entry.options[CONF_NAME], + unique_id=entry.unique_id, data=new_data, + title=new_data[CONF_NAME], options={}, ) # Update the calendar config - hass.data[DOMAIN][entry.data.get(CONF_NAME)].update_config(new_data) + hass.data[DOMAIN][entry.unique_id].update_config(new_data) class ICalEvents: @@ -151,6 +181,7 @@ def __init__(self, hass, config): self.code_generator = config.get(CONF_CODE_GENERATION, DEFAULT_CODE_GENERATION) self.event = None self.all_day = False + self.created = config.get(CONF_CREATION_DATETIME, str(dt.now())) async def async_get_events( self, hass, start_date, end_date diff --git a/custom_components/rental_control/calendar.py b/custom_components/rental_control/calendar.py index 7481447..f1371fb 100644 --- a/custom_components/rental_control/calendar.py +++ b/custom_components/rental_control/calendar.py @@ -24,7 +24,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity_id = generate_entity_id(ENTITY_ID_FORMAT, DOMAIN + " " + name, hass=hass) - rental_control_events = hass.data[DOMAIN][name] + rental_control_events = hass.data[DOMAIN][config_entry.unique_id] calendar = ICalCalendarEventDevice(hass, name, entity_id, rental_control_events) diff --git a/custom_components/rental_control/config_flow.py b/custom_components/rental_control/config_flow.py index 081f4a7..0ec48aa 100644 --- a/custom_components/rental_control/config_flow.py +++ b/custom_components/rental_control/config_flow.py @@ -1,5 +1,4 @@ """Config flow for Rental Control integration.""" -import asyncio import logging import re from typing import Any @@ -25,6 +24,7 @@ from .const import CONF_CHECKIN from .const import CONF_CHECKOUT from .const import CONF_CODE_GENERATION +from .const import CONF_CREATION_DATETIME from .const import CONF_DAYS from .const import CONF_EVENT_PREFIX from .const import CONF_IGNORE_NON_RESERVED @@ -44,6 +44,7 @@ from .const import DOMAIN from .const import LOCK_MANAGER from .const import REQUEST_TIMEOUT +from .util import gen_uuid _LOGGER = logging.getLogger(__name__) @@ -55,7 +56,7 @@ class RentalControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle the config flow for Rental Control.""" - VERSION = 1 + VERSION = 2 DEFAULTS = { CONF_CHECKIN: DEFAULT_CHECKIN, @@ -69,11 +70,14 @@ class RentalControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_VERIFY_SSL: True, } - async def _get_unique_name_error(self, user_input) -> Dict[str, str]: - """Check if name is unique, returning dictionary if so.""" - # Validate that Rental control is unique + def __init__(self): + """Setup the RentalControlFlowHandler.""" + self.created = str(dt.now()) + + async def _get_unique_id(self, user_input) -> Dict[str, str]: + """Generate the unique_id.""" existing_entry = await self.async_set_unique_id( - user_input[CONF_NAME], raise_on_progress=True + gen_uuid(self.created), raise_on_progress=True ) if existing_entry: return {CONF_NAME: "same_name"} @@ -103,16 +107,6 @@ def __init__(self, config_entry: config_entries.ConfigEntry): """Initialize Options Flow.""" self.config_entry = config_entry - def _get_unique_name_error(self, user_input) -> Dict[str, str]: - """Check if name is unique, returning dictionary if so.""" - # If name has changed, make sure new name isn't already being used - # otherwise show an error - if self.config_entry.unique_id != user_input[CONF_NAME]: - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == user_input[CONF_NAME]: - return {CONF_NAME: "same_name"} - return {} - async def async_step_init( self, user_input: Dict[str, Any] = None, @@ -273,12 +267,9 @@ async def _start_config_flow( description_placeholders = {} if user_input is not None: - # Regular flow has an async function, options flow has a sync function - # so we need to handle them conditionally - if asyncio.iscoroutinefunction(cls._get_unique_name_error): - errors.update(await cls._get_unique_name_error(user_input)) - else: - errors.update(cls._get_unique_name_error(user_input)) + # Regular flow has an async function + if hasattr(cls, "_get_unique_id"): + errors.update(await cls._get_unique_id(user_input)) # Validate user input try: @@ -331,6 +322,9 @@ async def _start_config_flow( ident=user_input[CONF_CODE_GENERATION], to_type=True ) + if hasattr(cls, "created"): + user_input[CONF_CREATION_DATETIME] = cls.created + 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 9877da7..5f06b9b 100644 --- a/custom_components/rental_control/const.py +++ b/custom_components/rental_control/const.py @@ -31,6 +31,7 @@ CONF_REFRESH_FREQUENCY = "refresh_frequency" CONF_START_SLOT = "start_slot" CONF_TIMEZONE = "timezone" +CONF_CREATION_DATETIME = "creation_datetime" # Defaults DEFAULT_CHECKIN = "16:00" diff --git a/custom_components/rental_control/sensor.py b/custom_components/rental_control/sensor.py index bb967a8..2d72229 100644 --- a/custom_components/rental_control/sensor.py +++ b/custom_components/rental_control/sensor.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name = config.get(CONF_NAME) max_events = config.get(CONF_MAX_EVENTS) - rental_control_events = hass.data[DOMAIN][name] + rental_control_events = hass.data[DOMAIN][config_entry.unique_id] await rental_control_events.update() if rental_control_events.calendar is None: _LOGGER.error("Unable to fetch iCal")