Skip to content

Commit 8855c30

Browse files
samspade21Brian McFarlane
andauthored
Release v1.5.0 (#41)
* 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) * chore: prepare release v1.5.0 * fix: Resolve linting failures for PR #41 - Add missing newline at end of .env.example file - Fix line length violations in api_client.py (lines 354, 361, 367) - Fix line length violations in sensor.py (lines 909, 920, 1110, 1124) All changes break long lines and comments to comply with 100-character limit. Addresses GitHub Actions pre-commit check failures. * fix: Use pre-commit-config-ci.yaml for CI checks The CI workflow was using the local .pre-commit-config.yaml which has auto-fix enabled. This caused ruff-format to modify files during CI, making the checks fail. Changed to use .pre-commit-config-ci.yaml which has check-only mode for ruff-format, matching the intended CI behavior. * fix: Apply ruff formatting to sensor.py Removed unnecessary else clause after return statements in native_value property. This fixes the ruff-format pre-commit check that was failing in CI. --------- Co-authored-by: Brian McFarlane <bmcfarlane@dataminr.com> Co-authored-by: samspade21 <samspade21@users.noreply.github.com>
1 parent 42ba619 commit 8855c30

File tree

11 files changed

+102
-83
lines changed

11 files changed

+102
-83
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ HA_SERVER_USER=your_ssh_user
1818
# Home Assistant config directory (optional)
1919
# Default: /homeassistant
2020
# Uncomment and modify if your config directory is in a different location
21-
# HA_CONFIG_DIR=/homeassistant
21+
# HA_CONFIG_DIR=/homeassistant

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
pip install -r requirements.txt
2828
2929
- name: Run pre-commit (ruff, mypy)
30-
run: pre-commit run --all-files
30+
run: pre-commit run --all-files --config .pre-commit-config-ci.yaml
3131

3232
- name: Install test dependencies
3333
run: pip install pytest pytest-cov pytest-homeassistant-custom-component

CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.5.0] - 2025-11-12
9+
10+
### Added
11+
- **Next Stay Sensor**: New [`VacasaNextStaySensor`](custom_components/vacasa/sensor.py) for tracking upcoming and current reservations
12+
- Displays next upcoming or current reservation with human-readable state messages
13+
- Comprehensive attributes: check-in/check-out dates, stay type, guest information
14+
- Computed values: days until check-in/check-out, stay duration nights
15+
- Supports guest, owner, maintenance, and block stay classifications
16+
- PR #32, #33, #34
17+
- **API Throttling**: Implemented sensor update throttling to respect coordinator refresh interval
18+
- Prevents excessive API calls for multiple sensors
19+
- All API-backed sensors now coordinate updates efficiently
20+
21+
### Fixed
22+
- **Critical Entity Registration Fix**: Added entity_id validation before state writes to prevent AttributeError during initialization (PR #38)
23+
- **JSON Parsing Enhancement**: Handle charset in API content-type headers (`application/json; charset=utf-8`) with fallback parsing and diagnostic logging
24+
- **Missing Import Fix**: Added missing `random` import in [`cached_data.py`](custom_components/vacasa/cached_data.py) required for retry jitter calculations
25+
- **Occupancy Sensor Alignment**: Fixed occupancy sensor to stay properly aligned with reservation data (PR #37)
26+
- **Token Handling**: Resolved token access issues and cleaned up cache imports (PR #35)
27+
28+
### Improved
29+
- **API Resilience**: Enhanced API client with better error handling, retry logic, and rich sensor support (PR #25)
30+
- **Blocking I/O Performance**: Refactored blocking I/O helpers for improved performance (PR #28)
31+
- **Sensor Setup Efficiency**: Streamlined sensor platform setup, reducing code by 99 lines (PR #27)
32+
- **Configuration Simplification**: Removed unused stay_type_mapping and API version configuration options
33+
34+
### Changed
35+
- **Dependency Updates**: Updated project dependencies to latest versions (PR #26, #30)
36+
- **Code Quality**: Fixed linting errors for improved CI/CD compliance
37+
- **Coordinator Integration**: Statement sensor now uses coordinator updates for better consistency
38+
- **Diagnostic Logging**: Enhanced logging for VacasaNextStaySensor troubleshooting
39+
40+
### Technical Improvements
41+
- All sensors now properly coordinate with data update coordinator
42+
- Enhanced error recovery and diagnostic capabilities
43+
- Improved code maintainability and reduced technical debt
44+
- Better separation of concerns in sensor architecture
45+
46+
This release introduces the highly-requested next stay sensor feature while delivering critical bug fixes for production stability and enhanced API resilience.
47+
848
## [1.4.2] - 2025-08-19
949

1050
### Testing

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.4.2
1+
1.5.0

custom_components/vacasa/api_client.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,14 @@
1919
AUTH_URL,
2020
DEFAULT_API_VERSION,
2121
DEFAULT_CACHE_TTL,
22+
DEFAULT_CLIENT_ID,
2223
DEFAULT_CONN_TIMEOUT,
2324
DEFAULT_JITTER_MAX,
2425
DEFAULT_KEEPALIVE_TIMEOUT,
2526
DEFAULT_MAX_CONNECTIONS,
2627
DEFAULT_READ_TIMEOUT,
27-
DEFAULT_CLIENT_ID,
2828
DEFAULT_TIMEOUT,
2929
MAX_RETRIES,
30-
SUPPORTED_API_VERSIONS,
3130
PROPERTY_CACHE_FILE,
3231
RETRY_BACKOFF_MULTIPLIER,
3332
RETRY_DELAY,
@@ -37,6 +36,7 @@
3736
STAY_TYPE_OTHER,
3837
STAY_TYPE_OWNER,
3938
STAY_TYPE_TO_CATEGORY,
39+
SUPPORTED_API_VERSIONS,
4040
TOKEN_CACHE_FILE,
4141
TOKEN_REFRESH_MARGIN,
4242
)
@@ -199,9 +199,7 @@ async def _retrieve_client_id(self) -> str | None:
199199
try:
200200
async with session.get(AUTH_URL, timeout=DEFAULT_TIMEOUT) as response:
201201
if response.status != 200:
202-
_LOGGER.warning(
203-
"Failed to fetch login page for client ID: %s", response.status
204-
)
202+
_LOGGER.warning("Failed to fetch login page for client ID: %s", response.status)
205203
return None
206204

207205
html = await response.text()
@@ -211,7 +209,7 @@ async def _retrieve_client_id(self) -> str | None:
211209

212210
patterns = [
213211
r'data-client-id="([A-Za-z0-9_-]+)"',
214-
r'client_id=([A-Za-z0-9_-]+)',
212+
r"client_id=([A-Za-z0-9_-]+)",
215213
]
216214

217215
for pattern in patterns:
@@ -226,8 +224,7 @@ async def _ensure_client_id(self) -> str:
226224
"""Ensure the OAuth client ID is up to date."""
227225
now = time.time()
228226
cache_valid = (
229-
self._client_id_last_fetch is not None
230-
and now - self._client_id_last_fetch < 3600
227+
self._client_id_last_fetch is not None and now - self._client_id_last_fetch < 3600
231228
)
232229

233230
if cache_valid and self._client_id:
@@ -287,7 +284,6 @@ async def ensure_session(self) -> aiohttp.ClientSession:
287284

288285
async def _run_blocking_io(self, func: Callable[..., T], *args, **kwargs) -> T:
289286
"""Execute a blocking IO function safely in the event loop."""
290-
291287
if self._hass:
292288
return await self._hass.async_add_executor_job(func, *args, **kwargs)
293289
return func(*args, **kwargs)
@@ -333,7 +329,6 @@ async def _request(
333329
retry_on_unauthorized: bool = True,
334330
) -> Any:
335331
"""Perform an HTTP request with API version fallback and error handling."""
336-
337332
session = await self.ensure_session()
338333
last_error: Exception | None = None
339334

@@ -353,27 +348,27 @@ async def _request(
353348
if not return_json:
354349
return await response.text()
355350
# Always attempt JSON parsing when return_json=True
356-
# API may include charset in content-type (e.g., "application/json; charset=utf-8")
351+
# API may include charset in content-type
352+
# (e.g., "application/json; charset=utf-8")
357353
try:
358354
return await response.json()
359355
except (aiohttp.ContentTypeError, json.JSONDecodeError) as e:
360356
# Log diagnostic info for troubleshooting
361357
response_text = await response.text()
362358
_LOGGER.warning(
363-
"Failed to parse JSON response from %s (content-type: %s): %s. Response: %s",
359+
"Failed to parse JSON from %s (content-type: %s): %s. Response: %s",
364360
url,
365361
response.content_type,
366362
e,
367363
response_text[:200],
368364
)
369-
# Return text as fallback, but this will likely cause errors in calling code
365+
# Return text as fallback, but this will likely
366+
# cause errors in calling code
370367
return response_text
371368

372369
if response.status == 401:
373370
# Attempt token refresh once when unauthorized
374-
_LOGGER.warning(
375-
"API request unauthorized for %s, refreshing token", url
376-
)
371+
_LOGGER.warning("API request unauthorized for %s, refreshing token", url)
377372
if retry_on_unauthorized:
378373
await self.authenticate()
379374
await self._save_token_to_cache()
@@ -1116,7 +1111,6 @@ async def _fetch_unit_details():
11161111

11171112
async def get_home_info(self, unit_id: str) -> dict[str, Any]:
11181113
"""Return the detailed home-info payload for a unit."""
1119-
11201114
await self.ensure_token()
11211115
owner_id = await self.get_owner_id()
11221116

@@ -1131,7 +1125,6 @@ async def get_statements(
11311125
self, year: int | None = None, month: int | None = None
11321126
) -> list[dict[str, Any]]:
11331127
"""Fetch owner statements, optionally scoped to a specific month."""
1134-
11351128
await self.ensure_token()
11361129
owner_id = await self.get_owner_id()
11371130

@@ -1153,7 +1146,6 @@ async def get_maintenance(
11531146
self, unit_id: str, status: str | None = "open"
11541147
) -> list[dict[str, Any]]:
11551148
"""Fetch maintenance tickets for a unit."""
1156-
11571149
await self.ensure_token()
11581150
owner_id = await self.get_owner_id()
11591151

custom_components/vacasa/binary_sensor.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,7 @@ async def _update_current_and_next_events(self) -> None:
336336

337337
# Check if there's a current event
338338
if calendar_state.state == "on":
339-
current_event = self._create_event_from_attributes(
340-
message, start_attr, end_attr
341-
)
339+
current_event = self._create_event_from_attributes(message, start_attr, end_attr)
342340

343341
if current_event:
344342
self._current_event = current_event
@@ -355,9 +353,7 @@ async def _update_current_and_next_events(self) -> None:
355353
self._name,
356354
)
357355
else:
358-
next_event = self._create_event_from_attributes(
359-
message, start_attr, end_attr
360-
)
356+
next_event = self._create_event_from_attributes(message, start_attr, end_attr)
361357

362358
if next_event:
363359
self._next_event = next_event
@@ -372,10 +368,8 @@ async def _update_current_and_next_events(self) -> None:
372368
next_event_attr = calendar_state.attributes.get("next_event")
373369
if isinstance(next_event_attr, dict):
374370
alt_next_event = self._create_event_from_attributes(
375-
next_event_attr.get("summary")
376-
or next_event_attr.get("message", ""),
377-
next_event_attr.get("start")
378-
or next_event_attr.get("start_time"),
371+
next_event_attr.get("summary") or next_event_attr.get("message", ""),
372+
next_event_attr.get("start") or next_event_attr.get("start_time"),
379373
next_event_attr.get("end") or next_event_attr.get("end_time"),
380374
)
381375

@@ -598,16 +592,8 @@ def _create_event_from_attributes(
598592
return None
599593

600594
try:
601-
start_dt = (
602-
dt_util.parse_datetime(start)
603-
if isinstance(start, str)
604-
else start
605-
)
606-
end_dt = (
607-
dt_util.parse_datetime(end)
608-
if isinstance(end, str)
609-
else end
610-
)
595+
start_dt = dt_util.parse_datetime(start) if isinstance(start, str) else start
596+
end_dt = dt_util.parse_datetime(end) if isinstance(end, str) else end
611597

612598
if not start_dt or not end_dt:
613599
return None

custom_components/vacasa/cached_data.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ async def cleanup_expired(self) -> int:
151151

152152
async def _run_io_task(self, func: Callable[..., T], *args, **kwargs) -> T:
153153
"""Execute a blocking IO task safely when hass is available."""
154-
155154
if self._hass:
156155
return await self._hass.async_add_executor_job(func, *args, **kwargs)
157156
return func(*args, **kwargs)

custom_components/vacasa/config_flow.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Config flow for Vacasa integration."""
22

3-
import json
43
import logging
54
import re
65
from typing import Any, Dict, Optional

custom_components/vacasa/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
"iot_class": "cloud_polling",
99
"issue_tracker": "https://github.com/samspade21/vacasa-ha/issues",
1010
"requirements": ["aiohttp>=3.8.0"],
11-
"version": "1.4.2"
11+
"version": "1.5.0"
1212
}

0 commit comments

Comments
 (0)