Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- **Crash on setup when no devices are usable or initial fetch fails**: replaced all uses of the removed `hass.components.persistent_notification` accessor with the current `homeassistant.components.persistent_notification.async_create(hass, ...)` API. Previously, any code path that tried to show a persistent notification (no devices found, initial connection failure, invalid/read-only property, failed switch/select control) raised `AttributeError: 'HomeAssistant' object has no attribute 'components'` on modern Home Assistant, aborting integration setup entirely instead of surfacing the intended message (#8)

## [0.5.0] - 2026-04-10

### Changed
Expand Down
58 changes: 37 additions & 21 deletions custom_components/pecron/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import timedelta
from typing import Final

from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant, ServiceCall
Expand Down Expand Up @@ -42,9 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data.get(CONF_REFRESH_INTERVAL, DEFAULT_REFRESH_INTERVAL),
)

coordinator = PecronDataUpdateCoordinator(
hass, email, password, region, refresh_interval
)
coordinator = PecronDataUpdateCoordinator(hass, email, password, region, refresh_interval)

# Attempt initial refresh with retry logic
max_retries = 3
Expand All @@ -71,7 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
max_retries,
)
# Show notification about the failure
hass.components.persistent_notification.async_create(
persistent_notification.async_create(
hass,
f"Failed to connect to Pecron API after {max_retries} attempts. "
"Please check your internet connection and credentials, then reload the integration.",
title="Pecron: Connection Failed",
Expand All @@ -82,7 +82,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

# Show persistent notification if no devices found
if coordinator.data is not None and not coordinator.data:
hass.components.persistent_notification.async_create(
persistent_notification.async_create(
hass,
"No Pecron devices found on your account. "
"Please check that devices are registered in the Pecron mobile app and try reloading the integration.",
title="Pecron: No Devices Found",
Expand Down Expand Up @@ -156,7 +157,8 @@ async def async_handle_set_property(call: ServiceCall) -> None:
device.device_name,
list(tsl_property_codes.keys()),
)
hass.components.persistent_notification.async_create(
persistent_notification.async_create(
hass,
f"Property '{property_code}' is not supported by {device.device_name}. "
f"Available properties: {', '.join(sorted(tsl_property_codes.keys()))}",
title="Pecron: Invalid Property",
Expand All @@ -172,7 +174,8 @@ async def async_handle_set_property(call: ServiceCall) -> None:
property_code,
device.device_name,
)
hass.components.persistent_notification.async_create(
persistent_notification.async_create(
hass,
f"Property '{property_code}' is read-only and cannot be modified.",
title="Pecron: Read-Only Property",
notification_id=f"{DOMAIN}_readonly_property_{device_key}",
Expand Down Expand Up @@ -207,7 +210,8 @@ async def async_handle_set_property(call: ServiceCall) -> None:
device.device_name,
result.message or "Unknown error",
)
hass.components.persistent_notification.async_create(
persistent_notification.async_create(
hass,
f"Failed to set property '{property_code}' on {device.device_name}: "
f"{result.message or 'Unknown error'}",
title="Pecron: Property Set Failed",
Expand All @@ -222,7 +226,8 @@ async def async_handle_set_property(call: ServiceCall) -> None:
err,
exc_info=True,
)
hass.components.persistent_notification.async_create(
persistent_notification.async_create(
hass,
f"Error setting property '{property_code}' on {device.device_name}: {err}",
title="Pecron: Service Error",
notification_id=f"{DOMAIN}_set_property_error_{device_key}",
Expand Down Expand Up @@ -300,8 +305,13 @@ async def _async_update_data(self) -> dict:
last_error = err
error_str = str(err).lower()
# Differentiate error types for better diagnostics
if ("authentication" in error_str or "401" in error_str or "unauthorized" in error_str or
"5032" in error_str or "token" in error_str):
if (
"authentication" in error_str
or "401" in error_str
or "unauthorized" in error_str
or "5032" in error_str
or "token" in error_str
):
if attempt < max_retries - 1:
# Token likely expired - reset API to force re-login on next attempt
_LOGGER.warning(
Expand All @@ -325,7 +335,9 @@ async def _async_update_data(self) -> dict:
_LOGGER.warning("Connection error while communicating with Pecron API: %s", err)
raise UpdateFailed(f"Connection error: {err}") from err
else:
_LOGGER.error("Unexpected error communicating with Pecron API: %s", err, exc_info=True)
_LOGGER.error(
"Unexpected error communicating with Pecron API: %s", err, exc_info=True
)
raise UpdateFailed(f"Error communicating with Pecron API: {err}") from err

# If we exhausted retries without raising, raise the last error
Expand All @@ -349,15 +361,18 @@ def _fetch_data(self) -> dict:
self.devices = self.api.get_devices()

if is_refresh:
_LOGGER.info("Token refreshed successfully. Found %d Pecron device(s) on account", len(self.devices))
_LOGGER.info(
"Token refreshed successfully. Found %d Pecron device(s) on account",
len(self.devices),
)
else:
_LOGGER.info("Found %d Pecron device(s) on account", len(self.devices))

if not self.devices:
_LOGGER.warning(
"No Pecron devices found on account %s. "
"Please check that devices are registered in the Pecron app.",
self.email
self.email,
)
else:
for device in self.devices:
Expand Down Expand Up @@ -434,8 +449,13 @@ def _fetch_data(self) -> dict:
except Exception as err:
error_str = str(err).lower()
# Check if this is an authentication error that needs token refresh
if ("authentication" in error_str or "401" in error_str or "unauthorized" in error_str or
"5032" in error_str or "token" in error_str):
if (
"authentication" in error_str
or "401" in error_str
or "unauthorized" in error_str
or "5032" in error_str
or "token" in error_str
):
_LOGGER.warning(
"Authentication error fetching properties for %s: %s. Token may have expired.",
device.device_name,
Expand Down Expand Up @@ -475,11 +495,7 @@ def get_new_devices(self, existing_keys: set[str]) -> dict:
"""Get devices that are new since the given set of keys."""
if not self.data:
return {}
return {
key: value
for key, value in self.data.items()
if key not in existing_keys
}
return {key: value for key, value in self.data.items() if key not in existing_keys}

async def async_shutdown(self) -> None:
"""Shutdown the coordinator and close API connection."""
Expand Down
14 changes: 8 additions & 6 deletions custom_components/pecron/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from dataclasses import dataclass, field
from typing import Any

from homeassistant.components import persistent_notification
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
Expand Down Expand Up @@ -248,7 +249,8 @@ async def async_select_option(self, option: str) -> None:
api = self.coordinator.api
if api is None:
_LOGGER.error("API not available for %s", self._attr_name)
self.hass.components.persistent_notification.async_create(
persistent_notification.async_create(
self.hass,
f"Failed to control {self._attr_name}: API not initialized",
title="Pecron: Control Failed",
notification_id=f"{DOMAIN}_control_failed_{self._attr_unique_id}",
Expand All @@ -275,9 +277,7 @@ async def async_select_option(self, option: str) -> None:
self.async_write_ha_state()

try:
result = await self.hass.async_add_executor_job(
method, self._device, api_value
)
result = await self.hass.async_add_executor_job(method, self._device, api_value)

if not result.success:
_LOGGER.error(
Expand All @@ -288,7 +288,8 @@ async def async_select_option(self, option: str) -> None:
)
self._attr_current_option = old_option
self.async_write_ha_state()
self.hass.components.persistent_notification.async_create(
persistent_notification.async_create(
self.hass,
f"Failed to set {self._attr_name} to {option}: "
f"{result.error_message or 'Unknown error'}",
title="Pecron: Control Failed",
Expand All @@ -315,7 +316,8 @@ async def delayed_refresh(delay: int) -> None:
)
self._attr_current_option = old_option
self.async_write_ha_state()
self.hass.components.persistent_notification.async_create(
persistent_notification.async_create(
self.hass,
f"Error controlling {self._attr_name}: {err}",
title="Pecron: Control Error",
notification_id=f"{DOMAIN}_control_error_{self._attr_unique_id}",
Expand Down
19 changes: 12 additions & 7 deletions custom_components/pecron/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from dataclasses import dataclass
from typing import Any

from homeassistant.components import persistent_notification
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
Expand Down Expand Up @@ -78,7 +79,10 @@ def create_switches_for_device(device_key: str, device_data: dict) -> list:
for switch_desc in PECRON_SWITCHES:
# Check both the property name and the _hm variant
# (API maps ac_switch_hm -> ac_switch in properties)
if switch_desc.key in tsl_property_codes or f"{switch_desc.key}_hm" in tsl_property_codes:
if (
switch_desc.key in tsl_property_codes
or f"{switch_desc.key}_hm" in tsl_property_codes
):
switches.append(
PecronSwitch(
coordinator,
Expand Down Expand Up @@ -219,7 +223,8 @@ async def _async_set_state(self, enabled: bool) -> None:
api = self.coordinator.api
if api is None:
_LOGGER.error("API not available for %s", self._attr_name)
self.hass.components.persistent_notification.async_create(
persistent_notification.async_create(
self.hass,
f"Failed to control {self._attr_name}: API not initialized",
title="Pecron: Control Failed",
notification_id=f"{DOMAIN}_control_failed_{self._attr_unique_id}",
Expand All @@ -245,9 +250,7 @@ async def _async_set_state(self, enabled: bool) -> None:

try:
# Call the API method in executor
result = await self.hass.async_add_executor_job(
method, self._device, enabled
)
result = await self.hass.async_add_executor_job(method, self._device, enabled)

if not result.success:
_LOGGER.error(
Expand All @@ -259,7 +262,8 @@ async def _async_set_state(self, enabled: bool) -> None:
# Revert optimistic update on failure
self._attr_is_on = old_state
self.async_write_ha_state()
self.hass.components.persistent_notification.async_create(
persistent_notification.async_create(
self.hass,
f"Failed to {'turn on' if enabled else 'turn off'} {self._attr_name}: "
f"{result.message or 'Unknown error'}",
title="Pecron: Control Failed",
Expand Down Expand Up @@ -300,7 +304,8 @@ async def delayed_refresh(delay: int) -> None:
# Revert optimistic update on error
self._attr_is_on = old_state
self.async_write_ha_state()
self.hass.components.persistent_notification.async_create(
persistent_notification.async_create(
self.hass,
f"Error controlling {self._attr_name}: {err}",
title="Pecron: Control Error",
notification_id=f"{DOMAIN}_control_error_{self._attr_unique_id}",
Expand Down
Loading