Procházet zdrojové kódy

Remove merge layer and sensor defaults

Store is now the single source of truth. CalDAV result is authoritative
when reachable (clears slot on empty fetch). Thin updates apply_partial
onto the stored record directly. Options flow is a single CalDAV step.
ZiB před 4 měsíci
rodič
revize
000d5e1e3a

+ 2 - 25
custom_components/room_cal_ha/config_flow.py

@@ -28,8 +28,6 @@ from .const import (
   DEFAULT_NEXT_NAME,
   DEFAULT_NEXT_NAME,
   DEFAULT_ROOM_STATE_NAME,
   DEFAULT_ROOM_STATE_NAME,
   DOMAIN,
   DOMAIN,
-  SENSOR_KEYS,
-  default_option_key,
 )
 )
 
 
 
 
@@ -129,9 +127,8 @@ class RoomCalOptionsFlow(OptionsFlow):
           errors["base"] = "cannot_connect"
           errors["base"] = "cannot_connect"
 
 
       if not errors:
       if not errors:
-        self._pending_options = dict(self.config_entry.options)
-        self._pending_options.update(user_input)
-        return await self.async_step_defaults()
+        merged = {**self.config_entry.options, **user_input}
+        return self.async_create_entry(data=merged)
 
 
     opts = self.config_entry.options
     opts = self.config_entry.options
     return self.async_show_form(
     return self.async_show_form(
@@ -144,26 +141,6 @@ class RoomCalOptionsFlow(OptionsFlow):
       errors=errors,
       errors=errors,
     )
     )
 
 
-  async def async_step_defaults(
-    self, user_input: dict[str, Any] | None = None
-  ) -> ConfigFlowResult:
-    if user_input is not None:
-      self._pending_options.update(
-        {default_option_key(k): v or None for k, v in user_input.items()}
-      )
-      return self.async_create_entry(data=self._pending_options)
-
-    opts = self.config_entry.options
-    schema_dict: dict[Any, Any] = {}
-    for key in SENSOR_KEYS:
-      opt_key = default_option_key(key)
-      schema_dict[vol.Optional(key, default=opts.get(opt_key, ""))] = str
-
-    return self.async_show_form(
-      step_id="defaults",
-      data_schema=vol.Schema(schema_dict),
-    )
-
 
 
 def _test_caldav_auth(url: str, username: str, password: str) -> None:
 def _test_caldav_auth(url: str, username: str, password: str) -> None:
   """Blocking: verify credentials by fetching the principal."""
   """Blocking: verify credentials by fetching the principal."""

+ 0 - 6
custom_components/room_cal_ha/const.py

@@ -30,12 +30,6 @@ SENSOR_KEYS: list[str] = [
   "door_code",
   "door_code",
 ]
 ]
 
 
-# entry.options key prefix for admin-configured sensor defaults
-DEFAULT_KEY_PREFIX = "default_"
-
-def default_option_key(sensor_key: str) -> str:
-  return f"{DEFAULT_KEY_PREFIX}{sensor_key}"
-
 # Default device names (suggested in config flow)
 # Default device names (suggested in config flow)
 DEFAULT_ROOM_STATE_NAME = "Room State"
 DEFAULT_ROOM_STATE_NAME = "Room State"
 DEFAULT_CURRENT_NAME = "Current"
 DEFAULT_CURRENT_NAME = "Current"

+ 10 - 32
custom_components/room_cal_ha/coordinator.py

@@ -25,13 +25,11 @@ from .const import (
   DOMAIN,
   DOMAIN,
   RESERVATION_TITLE_PREFIX,
   RESERVATION_TITLE_PREFIX,
   SCAN_INTERVAL_MINUTES,
   SCAN_INTERVAL_MINUTES,
-  SENSOR_KEYS,
   STORE_CURRENT,
   STORE_CURRENT,
   STORE_NEXT,
   STORE_NEXT,
   X_DATA_PROP,
   X_DATA_PROP,
-  default_option_key,
 )
 )
-from .models import ReservationData, merge
+from .models import ReservationData
 from .store import ReservationStore
 from .store import ReservationStore
 
 
 _LOGGER = logging.getLogger(__name__)
 _LOGGER = logging.getLogger(__name__)
@@ -68,49 +66,34 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
   def inject_guest_mode_switch(self, switch: Any) -> None:
   def inject_guest_mode_switch(self, switch: Any) -> None:
     self._guest_mode_switch = switch
     self._guest_mode_switch = switch
 
 
-  def _option_defaults(self) -> ReservationData:
-    opts = self.entry.options
-    kwargs: dict[str, Any] = {}
-    for key in SENSOR_KEYS:
-      kwargs[key] = opts.get(default_option_key(key))
-    return ReservationData(**kwargs)
-
   async def _async_update_data(self) -> CoordinatorData:
   async def _async_update_data(self) -> CoordinatorData:
-    option_defaults = self._option_defaults()
-
     caldav_current: ReservationData | None = None
     caldav_current: ReservationData | None = None
     caldav_next: ReservationData | None = None
     caldav_next: ReservationData | None = None
+    caldav_fetched = False
 
 
     if self.entry.options.get(CONF_CALDAV_ENABLED):
     if self.entry.options.get(CONF_CALDAV_ENABLED):
       try:
       try:
         caldav_current, caldav_next = await self.hass.async_add_executor_job(
         caldav_current, caldav_next = await self.hass.async_add_executor_job(
           self._fetch_caldav
           self._fetch_caldav
         )
         )
+        caldav_fetched = True
       except caldav_error.AuthorizationError:
       except caldav_error.AuthorizationError:
         self.entry.async_start_reauth(self.hass)
         self.entry.async_start_reauth(self.hass)
         raise UpdateFailed("CalDAV authorization failed; re-auth required")
         raise UpdateFailed("CalDAV authorization failed; re-auth required")
       except Exception as exc:
       except Exception as exc:
         _LOGGER.warning("CalDAV fetch failed, using cached data: %s", exc)
         _LOGGER.warning("CalDAV fetch failed, using cached data: %s", exc)
 
 
-    stored_current = self.store.get(STORE_CURRENT)
-    stored_next = self.store.get(STORE_NEXT)
-
     now = dt_util.utcnow()
     now = dt_util.utcnow()
 
 
-    # When CalDAV returned data, always persist it and refresh the timestamp.
-    # When CalDAV was skipped/failed, serve from store without touching the timestamp.
-    if caldav_current is not None:
+    if caldav_fetched:
+      # Calendar is authoritative: write its result including None (clears the slot).
       await self.store.async_set(STORE_CURRENT, caldav_current)
       await self.store.async_set(STORE_CURRENT, caldav_current)
-      stored_current = caldav_current
-      self._current_last_refresh = now
-
-    if caldav_next is not None:
       await self.store.async_set(STORE_NEXT, caldav_next)
       await self.store.async_set(STORE_NEXT, caldav_next)
-      stored_next = caldav_next
+      self._current_last_refresh = now
       self._next_last_refresh = now
       self._next_last_refresh = now
 
 
-    new_current = merge(option_defaults, stored_current)
-    new_next = merge(option_defaults, stored_next)
+    new_current = self.store.get(STORE_CURRENT) or ReservationData()
+    new_next = self.store.get(STORE_NEXT) or ReservationData()
 
 
     self._reschedule_mode_sync(new_current)
     self._reschedule_mode_sync(new_current)
 
 
@@ -229,20 +212,15 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
     updated = stored.apply_partial(partial)
     updated = stored.apply_partial(partial)
     await self.store.async_set(target, updated)
     await self.store.async_set(target, updated)
 
 
-    option_defaults = self._option_defaults()
     now = dt_util.utcnow()
     now = dt_util.utcnow()
-
     if target == STORE_CURRENT:
     if target == STORE_CURRENT:
       self._current_last_refresh = now
       self._current_last_refresh = now
     else:
     else:
       self._next_last_refresh = now
       self._next_last_refresh = now
 
 
-    current = merge(option_defaults, self.store.get(STORE_CURRENT))
-    nxt = merge(option_defaults, self.store.get(STORE_NEXT))
-
     self.async_set_updated_data(CoordinatorData(
     self.async_set_updated_data(CoordinatorData(
-      current=current,
-      next=nxt,
+      current=self.store.get(STORE_CURRENT) or ReservationData(),
+      next=self.store.get(STORE_NEXT) or ReservationData(),
       current_last_refresh=self._current_last_refresh,
       current_last_refresh=self._current_last_refresh,
       next_last_refresh=self._next_last_refresh,
       next_last_refresh=self._next_last_refresh,
     ))
     ))

+ 0 - 27
custom_components/room_cal_ha/models.py

@@ -56,30 +56,3 @@ class ReservationData:
       else:
       else:
         current[k] = v
         current[k] = v
     return ReservationData.from_dict(current)
     return ReservationData.from_dict(current)
-
-
-def merge(
-  option_defaults: ReservationData,
-  caldav_data: ReservationData | None,
-  overrides: ReservationData | None = None,
-) -> ReservationData:
-  """Merge three layers lowest-to-highest: option_defaults < caldav_data < overrides.
-
-  A None field at a given layer means "not set at this layer"; the next lower layer's
-  value is used. Explicit None stored in a lower layer can be overridden by a non-None
-  value in a higher layer.
-  """
-  result: dict[str, Any] = {}
-  for f in fields(ReservationData):
-    key = f.name
-    val = getattr(option_defaults, key)
-    if caldav_data is not None:
-      layer = getattr(caldav_data, key)
-      if layer is not None:
-        val = layer
-    if overrides is not None:
-      layer = getattr(overrides, key)
-      if layer is not None:
-        val = layer
-    result[key] = val
-  return ReservationData(**result)

+ 2 - 2
custom_components/room_cal_ha/store.py

@@ -36,6 +36,6 @@ class ReservationStore:
       return None
       return None
     return ReservationData.from_dict(raw)
     return ReservationData.from_dict(raw)
 
 
-  async def async_set(self, key: str, data: ReservationData) -> None:
-    self._data[key] = data.to_dict()
+  async def async_set(self, key: str, data: ReservationData | None) -> None:
+    self._data[key] = data.to_dict() if data is not None else None
     await self.async_save()
     await self.async_save()

+ 0 - 14
custom_components/room_cal_ha/strings.json

@@ -36,20 +36,6 @@
           "caldav_url": "CalDAV URL",
           "caldav_url": "CalDAV URL",
           "calendar_name": "Calendar name"
           "calendar_name": "Calendar name"
         }
         }
-      },
-      "defaults": {
-        "title": "Sensor Defaults",
-        "description": "Optional fallback values used when a field is not provided by the calendar or a thin update.",
-        "data": {
-          "guest_name": "Guest Name",
-          "check_in_date": "Check-In Date",
-          "check_in_time": "Check-In Time",
-          "check_out_date": "Check-Out Date",
-          "check_out_time": "Check-Out Time",
-          "wifi_ssid": "WiFi SSID",
-          "wifi_key": "WiFi Key",
-          "door_code": "Door Code"
-        }
       }
       }
     },
     },
     "error": {
     "error": {

+ 0 - 14
custom_components/room_cal_ha/translations/en.json

@@ -36,20 +36,6 @@
           "caldav_url": "CalDAV URL",
           "caldav_url": "CalDAV URL",
           "calendar_name": "Calendar name"
           "calendar_name": "Calendar name"
         }
         }
-      },
-      "defaults": {
-        "title": "Sensor Defaults",
-        "description": "Optional fallback values used when a field is not provided by the calendar or a thin update.",
-        "data": {
-          "guest_name": "Guest Name",
-          "check_in_date": "Check-In Date",
-          "check_in_time": "Check-In Time",
-          "check_out_date": "Check-Out Date",
-          "check_out_time": "Check-Out Time",
-          "wifi_ssid": "WiFi SSID",
-          "wifi_key": "WiFi Key",
-          "door_code": "Door Code"
-        }
       }
       }
     },
     },
     "error": {
     "error": {