Skip to content

Commit 42ba619

Browse files
samspade21Brian McFarlane
andauthored
Development (#39)
* Release v1.4.2: End-to-end release pipeline validation - Test consolidated auto-release.yml workflow functionality - Validate VERSION detection and changelog extraction fixes - Confirm complete tag → release automation works properly - Verify v1.4.1 pipeline issues have been resolved * Chore/update dependencies (#26) * Release v1.4.2: End-to-end release pipeline validation (#22) - Test consolidated auto-release.yml workflow functionality - Validate VERSION detection and changelog extraction fixes - Confirm complete tag → release automation works properly - Verify v1.4.1 pipeline issues have been resolved Co-authored-by: Brian McFarlane <bmcfarlane@dataminr.com> * fix: grant permissions for dependency workflow (#24) * chore: update dependencies --------- Co-authored-by: Brian McFarlane <bmcfarlane@dataminr.com> Co-authored-by: samspade21 <samspade21@users.noreply.github.com> * Enhance Vacasa API resiliency and add rich sensors (#25) * refactor: streamline vacasa sensor setup (#27) * Refactor blocking IO helpers (#28) * Throttle API-backed Vacasa sensors to coordinator interval (#33) * feat(sensor): Add VacasaNextStaySensor with API throttling (#32) Implements next_stay sensor with comprehensive reservation attributes: - Displays next upcoming or current reservation - Provides detailed attributes: checkin/checkout dates, stay type, guest info - Computes days_until_checkin, days_until_checkout, stay_duration_nights - Supports guest, owner, maintenance, and block stay classifications - Human-readable state with contextual messages Changes: - Added SENSOR_NEXT_STAY constant to const.py - Added VacasaNextStaySensor class using VacasaApiUpdateMixin pattern - Integrated sensor into UNIT_SENSOR_CLASSES tuple - Adopts API throttling pattern from PR #33 for coordinator-synced updates Conflict resolution: - Resolved merge conflict in sensor.py from PR #33 - Refactored to use VacasaApiUpdateMixin for proper API throttling - Changed async_update() to _async_update_from_api() pattern * Use coordinator updates for statement sensor * Fix token handling access and clean cache imports (#35) * fix: keep occupancy sensor aligned with reservations (#37) * refactor: Remove stay_type_mapping and API version configuration options Simplifies integration configuration by removing custom stay type mapping and API version selection: Removed Configuration: - CONF_API_VERSION and CONF_STAY_TYPE_MAPPING constants - DEFAULT_STAY_TYPE_MAP constant (redundant) - API version and stay type mapping parameters from VacasaApiClient - stay_type_mapping logic from categorize_reservation (now uses direct checks) - API version and stay type mapping UI fields from config flow Kept Functionality: - Stay type constants (STAY_TYPE_GUEST, etc.) for categorization - STAY_TYPE_TO_CATEGORY and STAY_TYPE_TO_NAME mappings for internal use - API version fallback logic in api_client._request() - Automatic stay type detection based on reservation attributes Reasoning: - Simplifies user configuration (fewer options to manage) - Stay types are automatically detected from Vacasa API responses - API version fallback provides automatic resilience - Reduces maintenance burden of custom mapping configurations Files Modified: - const.py: Removed config constants and DEFAULT_STAY_TYPE_MAP - api_client.py: Removed stay_type_mapping parameter and simplified categorization - __init__.py: Removed configuration reading for removed options - config_flow.py: Removed UI fields for API version and stay type mapping * fix: Handle charset in API content-type header for JSON parsing The Vacasa API recently started including charset in content-type headers (e.g., 'application/json; charset=utf-8' instead of 'application/json'). The exact equality check in _request() was failing, causing the method to return unparsed text strings instead of JSON dictionaries. This caused 'string indices must be integers' errors at line 947 and other locations where code expected dictionary responses like data['data']. Changes: - Always attempt JSON parsing when return_json=True regardless of content-type - Catch both aiohttp.ContentTypeError and json.JSONDecodeError exceptions - Add diagnostic logging with URL, content-type, error, and response preview - Fall back to returning text if JSON parsing fails (preserves existing behavior) Fixes critical integration failure preventing all Vacasa sensors, binary sensors, and calendars from loading. Resolves: #36 * debug: Add diagnostic logging for VacasaNextStaySensor troubleshooting * fix: Critical bug fixes for entity registration, JSON parsing, and imports - fix(cached_data): Add missing random import * Required by RetryWithBackoff.calculate_delay() for jitter calculation * Prevents runtime NameError when retry logic is triggered - fix(api_client): Handle charset in API content-type headers for JSON parsing * API may return 'application/json; charset=utf-8' instead of plain 'application/json' * Added fallback JSON parsing with diagnostic logging for troubleshooting * Prevents ContentTypeError when parsing valid JSON responses - fix(sensor): Prevent premature state write before entity registration * Added entity_id check before calling async_write_ha_state() * Ensures entities are fully registered before state updates * Fixes AttributeError when sensors refresh during initialization All fixes tested and verified to resolve production issues. * style: Fix linting errors for CI/CD compliance - Fix E402: Move imports to top of file in api_client.py - Fix E501: Break long lines to comply with 100 char limit - Fix D107/D102: Add missing docstrings to class methods - Improve code formatting and readability Resolves pre-commit hook failures in GitHub Actions. * Handle all candidate calendar entity IDs during recovery (#40) --------- Co-authored-by: Brian McFarlane <bmcfarlane@dataminr.com> Co-authored-by: samspade21 <samspade21@users.noreply.github.com>
1 parent 2fafeb7 commit 42ba619

File tree

3 files changed

+37
-21
lines changed

3 files changed

+37
-21
lines changed

custom_components/vacasa/api_client.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
import aiohttp
1515

1616
from .cached_data import CachedData, RetryWithBackoff
17-
18-
T = TypeVar("T")
19-
20-
2117
from .const import (
2218
API_BASE_TEMPLATE,
2319
AUTH_URL,
@@ -45,6 +41,8 @@
4541
TOKEN_REFRESH_MARGIN,
4642
)
4743

44+
T = TypeVar("T")
45+
4846
_LOGGER = logging.getLogger(__name__)
4947

5048

@@ -354,7 +352,6 @@ async def _request(
354352
self._set_api_version(version)
355353
if not return_json:
356354
return await response.text()
357-
358355
# Always attempt JSON parsing when return_json=True
359356
# API may include charset in content-type (e.g., "application/json; charset=utf-8")
360357
try:

custom_components/vacasa/binary_sensor.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -232,18 +232,22 @@ async def _find_calendar_entity_with_retry(self, max_retries: int = 3) -> str |
232232
)
233233
return None
234234

235-
async def _find_calendar_entity(self) -> str | None:
236-
"""Find the corresponding calendar entity ID for this unit."""
237-
_LOGGER.debug("Searching for calendar entity for unit %s (%s)", self._unit_id, self._name)
238-
239-
# Generate possible entity IDs to try
235+
def _candidate_calendar_entity_ids(self) -> list[str]:
236+
"""Return all possible calendar entity IDs for this unit."""
240237
sanitized_name = self._sanitize_entity_name(self._name)
241-
possible_entity_ids = [
238+
return [
242239
f"calendar.vacasa_{sanitized_name}",
243240
f"calendar.vacasa_calendar_{self._unit_id}",
244241
f"calendar.vacasa_{self._unit_id}",
245242
]
246243

244+
async def _find_calendar_entity(self) -> str | None:
245+
"""Find the corresponding calendar entity ID for this unit."""
246+
_LOGGER.debug("Searching for calendar entity for unit %s (%s)", self._unit_id, self._name)
247+
248+
# Generate possible entity IDs to try
249+
possible_entity_ids = self._candidate_calendar_entity_ids()
250+
247251
# First try: Check state registry (faster)
248252
for entity_id in possible_entity_ids:
249253
state = self.hass.states.get(entity_id)
@@ -536,13 +540,12 @@ def _handle_calendar_state_change(self, event: Event) -> None:
536540
event_data = event.data
537541
entity_id = event_data.get("entity_id", "")
538542

539-
# Only respond to calendar entity state changes
540-
if not entity_id.startswith("calendar.vacasa_"):
541-
return
543+
# Only respond to calendar entity state changes we expect
544+
expected_entities: set[str] = set(self._candidate_calendar_entity_ids())
545+
if self._calendar_entity:
546+
expected_entities.add(self._calendar_entity)
542547

543-
# Check if this is our expected calendar entity
544-
expected_calendar_entity = f"calendar.vacasa_{self._sanitize_entity_name(self._name)}"
545-
if entity_id != expected_calendar_entity:
548+
if entity_id not in expected_entities:
546549
return
547550

548551
new_state = event_data.get("new_state")

custom_components/vacasa/sensor.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,29 @@ class VacasaApiUpdateMixin:
9595

9696
_refresh_task: asyncio.Task[None] | None
9797

98-
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002,ANN003 - pass through
98+
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002,ANN003
99+
"""Initialize API update mixin."""
99100
self._update_lock = asyncio.Lock()
100101
self._refresh_task = None
101102
super().__init__(*args, **kwargs)
102103
self._attr_should_poll = False
103104

104105
async def async_added_to_hass(self) -> None:
106+
"""Register coordinator listener when added to hass."""
105107
await super().async_added_to_hass()
106108
self.async_on_remove(
107109
self._coordinator.async_add_listener(self._handle_coordinator_refresh)
108110
)
109111
await self.async_update()
110112

111113
async def async_update(self) -> None:
114+
"""Update entity state from API."""
112115
task = self._ensure_refresh_task()
113116
if task is not None:
114117
await task
115118

116119
async def async_will_remove_from_hass(self) -> None:
120+
"""Clean up refresh task when removed from hass."""
117121
if self._refresh_task and not self._refresh_task.done():
118122
self._refresh_task.cancel()
119123
with suppress(asyncio.CancelledError):
@@ -654,6 +658,7 @@ def __init__(
654658
name: str,
655659
unit_attributes: dict[str, Any],
656660
) -> None:
661+
"""Initialize home info sensor."""
657662
super().__init__(
658663
coordinator=coordinator,
659664
unit_id=unit_id,
@@ -717,6 +722,7 @@ def __init__(
717722
unit_attributes: dict[str, Any],
718723
status: str = "open",
719724
) -> None:
725+
"""Initialize maintenance sensor."""
720726
super().__init__(
721727
coordinator=coordinator,
722728
unit_id=unit_id,
@@ -777,6 +783,7 @@ class VacasaStatementSensor(VacasaApiUpdateMixin, SensorEntity):
777783
"""Sensor exposing the latest owner statement totals."""
778784

779785
def __init__(self, coordinator, config_entry: VacasaConfigEntry) -> None:
786+
"""Initialize statement sensor."""
780787
super().__init__()
781788
self._coordinator = coordinator
782789
self._config_entry = config_entry
@@ -895,7 +902,9 @@ async def _async_update_from_api(self) -> None:
895902
try:
896903
# Get reservations starting from today
897904
today = datetime.now().strftime("%Y-%m-%d")
898-
future_date = (datetime.now() + timedelta(days=365)).strftime("%Y-%m-%d")
905+
future_date = (datetime.now() + timedelta(days=365)).strftime(
906+
"%Y-%m-%d"
907+
)
899908

900909
_LOGGER.debug("Fetching reservations for %s from %s to %s", self._unit_id, today, future_date)
901910
reservations = await self._coordinator.client.get_reservations(
@@ -911,10 +920,17 @@ async def _async_update_from_api(self) -> None:
911920
_LOGGER.debug("Next stay for %s: %s", self._unit_id, "found" if self._reservation else "none")
912921

913922
except (AuthenticationError, ApiError) as err:
914-
_LOGGER.warning("Unable to update next stay for %s: %s", self._name, err)
923+
_LOGGER.warning(
924+
"Unable to update next stay for %s: %s", self._name, err
925+
)
915926
self._reservation = None
916927
except Exception as err:
917-
_LOGGER.error("Unexpected error updating next stay for %s: %s", self._name, err, exc_info=True)
928+
_LOGGER.error(
929+
"Unexpected error updating next stay for %s: %s",
930+
self._name,
931+
err,
932+
exc_info=True,
933+
)
918934
self._reservation = None
919935

920936
def _find_next_stay(self, reservations: list[dict]) -> dict | None:

0 commit comments

Comments
 (0)