From 05aca50deafbcfddaaeb0924ebfa2c461572881d Mon Sep 17 00:00:00 2001 From: jsightler Date: Thu, 30 Jul 2026 19:10:59 -0400 Subject: [PATCH 1/2] Fix crash on setup from removed hass.components.persistent_notification hass.components was removed from modern Home Assistant core, so every persistent notification call (no devices found, connection failure, invalid/read-only property, failed switch/select control) raised AttributeError and aborted config entry setup entirely. Switch to homeassistant.components.persistent_notification.async_create(hass, ...). This was masking the real error message in GH#8 (API error 5014/5041: No permission), turning a soft failure into a hard setup crash. Co-Authored-By: Claude Sonnet 5 --- .beads/issues.jsonl | 2 +- CHANGELOG.md | 5 +++ custom_components/pecron/__init__.py | 58 ++++++++++++++++++---------- custom_components/pecron/select.py | 14 ++++--- custom_components/pecron/switch.py | 19 +++++---- 5 files changed, 63 insertions(+), 35 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index c92a7cf..c8a7a7b 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -7,7 +7,7 @@ {"id":"ha-pecron-85t","title":"Implement automatic token refresh on authentication failure","description":"Currently the integration only logs in once during initialization (line 162 in __init__.py). If the Pecron API token expires during normal operation, subsequent API calls fail with authentication errors and the integration stops working until manually reloaded.\n\nWhen an authentication error is detected in _async_update_data (lines 147-149), we should:\n\n1. Reset the API instance to None to trigger re-initialization\n2. Retry the _fetch_data operation which will automatically re-login\n3. Add retry logic with a maximum attempt count to avoid infinite loops\n4. Log the token refresh for debugging\n\nImplementation approach:\n- In _async_update_data, catch authentication errors\n- Set self.api = None to force re-initialization on next attempt\n- Use a retry mechanism (max 2 retries) to re-attempt the data fetch\n- On successful re-login, log 'Token refreshed successfully'\n- If re-login fails after retries, raise UpdateFailed as before\n\nFiles to modify:\n- custom_components/pecron/__init__.py (PecronDataUpdateCoordinator class)\n\nThis is a P0 bug because users will experience integration failures without understanding why, requiring manual intervention to restore functionality.","status":"closed","priority":0,"issue_type":"bug","owner":"jsightler@example.com","created_at":"2026-02-13T20:55:55Z","created_by":"jsightler","updated_at":"2026-02-13T20:57:07Z","closed_at":"2026-02-13T15:57:07Z","close_reason":"Implemented automatic token refresh with retry logic. Integration now handles expired tokens gracefully by resetting API connection and re-logging in automatically.","dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"ha-pecron-env","title":"Fix empty dict falsy check preventing entity creation","description":"Critical bug: In sensor.py line 106 and binary_sensor.py line 81, the check 'if coordinator.data:' treats an empty dict {} as falsy. This means if the API returns no devices or all property fetches fail, coordinator.data will be {} and no entities will be created at all. This is likely why the user sees no power station entities. Fix: Change to 'if coordinator.data is not None:' or 'if coordinator.data:' with explicit empty dict handling.","status":"closed","priority":0,"issue_type":"bug","owner":"jsightler@example.com","created_at":"2026-02-10T05:08:23Z","created_by":"jsightler","updated_at":"2026-02-10T05:12:06Z","closed_at":"2026-02-10T00:12:06Z","close_reason":"Fixed falsy check in sensor.py and binary_sensor.py. Changed from 'if coordinator.data:' to 'if coordinator.data is not None:' to properly handle empty dict. Added warning logs when no devices found.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ha-pecron-nst","title":"[GH#6] Cannot login, error \"cannot_connect\"","description":"## GitHub Issue\nhttps://github.com/jsight/ha-pecron/issues/6\n\n## Summary\nAfter HACS install, login fails almost immediately with error `cannot_connect`. Credentials work in the Android Pecron app.\n\n## Reporter\n@simifor / Simon Hurtado (2026-04-20)\n\n## Suspected cause\nUser may be outside the regions covered by the current endpoint options (only 3 region choices). May need additional API endpoints or better diagnostics for region/connectivity failures.\n\n## Acceptance criteria\n- [ ] Improve logging/diagnostics around cannot_connect during config flow\n- [ ] Confirm which API endpoints/regions are supported and whether more are needed\n- [ ] Fix or document region selection so valid credentials can log in from more locations\n- [ ] Surface a clearer error when the failure is endpoint/region related vs bad credentials","status":"open","priority":1,"issue_type":"bug","owner":"jsightler@example.com","created_at":"2026-07-30T22:55:52Z","created_by":"jsightler","updated_at":"2026-07-30T22:55:52Z","external_ref":"gh-6","labels":["auth","connectivity","github","region"],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ha-pecron-bn4","title":"[GH#8] API error 5014/5041: No permission when fetching properties","description":"## GitHub Issue\nhttps://github.com/jsight/ha-pecron/issues/8\n\n## Summary\nUser can add the Pecron integration and enter credentials, but property fetch fails with:\n`Error fetching properties for F5000LFP_32A8 (key: 5037CD3A3DA8): API error 5014: No permission`\n\nTitle says 5041; log body reports 5014 — both indicate a permission/auth problem after login.\n\n## Reporter\n@kf9ug (2026-07-15)\n\n## Device\nF5000LFP_32A8 (key: 5037CD3A3DA8)\n\n## Steps tried by reporter\n- Verified login/password match Pecron app\n- Changed password on app and re-entered in HA\n- Deleted and re-added credentials\n- Restarted Home Assistant\n\n## Acceptance criteria\n- [ ] Reproduce or identify root cause of API error 5014/5041 No permission\n- [ ] Fix auth/token/permission handling so property fetch works for valid credentials\n- [ ] Surface a clear user-facing error if the account truly lacks device permission\n- [ ] Verify with F5000LFP or equivalent device if possible","notes":"Opened PR #10 (branch fix/persistent-notification-crash) fixing the hass.components.persistent_notification crash. Underlying cloud-side 5014/5041 permission cause still needs the reporter's account to fully diagnose -- keeping issue open/in_progress pending PR merge and reporter feedback.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"jsightler","owner":"jsightler@example.com","created_at":"2026-07-30T22:55:49Z","created_by":"jsightler","updated_at":"2026-07-30T23:14:48Z","started_at":"2026-07-30T23:10:45Z","external_ref":"gh-8","labels":["api","auth","github"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ha-pecron-bn4","title":"[GH#8] API error 5014/5041: No permission when fetching properties","description":"## GitHub Issue\nhttps://github.com/jsight/ha-pecron/issues/8\n\n## Summary\nUser can add the Pecron integration and enter credentials, but property fetch fails with:\n`Error fetching properties for F5000LFP_32A8 (key: 5037CD3A3DA8): API error 5014: No permission`\n\nTitle says 5041; log body reports 5014 — both indicate a permission/auth problem after login.\n\n## Reporter\n@kf9ug (2026-07-15)\n\n## Device\nF5000LFP_32A8 (key: 5037CD3A3DA8)\n\n## Steps tried by reporter\n- Verified login/password match Pecron app\n- Changed password on app and re-entered in HA\n- Deleted and re-added credentials\n- Restarted Home Assistant\n\n## Acceptance criteria\n- [ ] Reproduce or identify root cause of API error 5014/5041 No permission\n- [ ] Fix auth/token/permission handling so property fetch works for valid credentials\n- [ ] Surface a clear user-facing error if the account truly lacks device permission\n- [ ] Verify with F5000LFP or equivalent device if possible","notes":"Root cause found: hass.components.persistent_notification was removed from modern Home Assistant core. Every notification call site in __init__.py/switch.py/select.py used the old accessor, so any path that tried to show a notification (no devices found, connection failure, etc.) raised AttributeError and aborted config entry setup with an unrelated traceback -- masking the real 5014/5041 permission message and crashing setup entirely. Fixed by switching to homeassistant.components.persistent_notification.async_create(hass, ...). This does not change the underlying Pecron cloud permission error itself (that's server-side/account-side), but it stops setup from crashing and lets the real error message reach the user/log.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"jsightler","owner":"jsightler@example.com","created_at":"2026-07-30T22:55:49Z","created_by":"jsightler","updated_at":"2026-07-30T23:10:47Z","started_at":"2026-07-30T23:10:45Z","external_ref":"gh-8","labels":["api","auth","github"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ha-pecron-qcn","title":"Release v0.3.2: Smart time sensor logic","description":"Release with intelligent time sensor state machine.\n\n## New Features\n- Smart Time to Full / Time to Empty sensors\n- State-based display logic (charging/discharging/idle/UPS)\n- Always show both sensors with N/A when not applicable\n- Comprehensive edge case handling\n\n## Testing\n- 18 new unit tests\n- All 43 tests passing","status":"closed","priority":1,"issue_type":"task","owner":"jsightler@example.com","created_at":"2026-02-15T22:06:33Z","created_by":"jsightler","updated_at":"2026-02-15T22:08:09Z","closed_at":"2026-02-15T17:08:09Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ha-pecron-afa","title":"Smart time-remaining sensor logic with charging/discharging state","description":"Improve time-remaining sensors with comprehensive state machine logic and testing.\n\n## State Diagram: Time Sensor Display Logic\n\n### Device States (based on power flow)\n\n1. **IDLE**: input_power == 0 AND output_power == 0\n2. **CHARGING_ONLY**: input_power \u003e 0 AND output_power == 0\n3. **DISCHARGING_ONLY**: input_power == 0 AND output_power \u003e 0\n4. **UPS_MODE**: input_power \u003e 0 AND output_power \u003e 0 (pass-through + battery ready)\n\n### Property Availability Matrix\n\nAPI can return:\n- Both remain_charging_time AND remain_discharging_time\n- Only remain_charging_time (remain_discharging_time missing)\n- Only remain_discharging_time (remain_charging_time missing)\n- Neither property present\n- Property present but value is None/0\n- Property present but value is negative (error condition)\n\n### Display Logic: Time to Full (remain_charging_time)\n\n| State | Property Available | Property Missing | Property = None/0 | Property \u003c 0 |\n|-------|-------------------|------------------|-------------------|--------------|\n| IDLE | N/A | N/A | N/A | N/A |\n| CHARGING_ONLY | Show value | N/A | N/A (or show 0?) | N/A |\n| DISCHARGING_ONLY | N/A | N/A | N/A | N/A |\n| UPS_MODE | Show value | N/A | N/A (or show 0?) | N/A |\n\n### Display Logic: Time to Empty (remain_discharging_time)\n\n| State | Property Available | Property Missing | Property = None/0 | Property \u003c 0 |\n|-------|-------------------|------------------|-------------------|--------------|\n| IDLE | N/A | N/A | N/A | N/A |\n| CHARGING_ONLY | N/A | N/A | N/A | N/A |\n| DISCHARGING_ONLY | Show value | N/A | N/A (or show 0?) | N/A |\n| UPS_MODE | Show value | N/A | N/A (or show 0?) | N/A |\n\n### Edge Cases to Handle\n\n1. **Power value missing**: If total_input_power or total_output_power is None\n - Treat as 0 (use coalescing)\n\n2. **Negative power values**: Device reports -5W sometimes\n - Treat as 0 (use max(0, value))\n\n3. **Small power values**: Measurement noise (1W, 2W)\n - Still use \u003e 0 threshold as user requested\n\n4. **Battery at 100% while charging**: remain_charging_time might be 0\n - Show 0 (not N/A) - user can see charging is complete\n\n5. **Battery fully depleted**: remain_discharging_time might be 0\n - Show 0 (not N/A) - battery is empty\n\n6. **Alternative property names**: \n - remain_time vs remain_discharging_time\n - Check both variants\n\n7. **Both time values show same number**: API bug?\n - Still apply state logic, don't try to detect this\n\n8. **Rapid state transitions**: Device switches between charging/discharging\n - Each update applies current state logic\n - No hysteresis needed\n\n## Implementation Requirements\n\n1. **Always create both sensors** - bypass TSL filtering\n2. **Smart state detection** from input/output power\n3. **Robust property access** - handle missing/None/negative\n4. **Clear logging** - log when returning N/A due to state\n5. **Unit tests** covering all state combinations\n\n## Test Coverage Needed\n\n- All 4 device states × both sensors = 8 core tests\n- Property missing scenarios = 4 tests \n- Property None/0 scenarios = 4 tests\n- Property negative scenarios = 2 tests\n- Power value missing/None = 4 tests\n- Edge cases (100% battery, 0% battery) = 4 tests\n- **Total: ~26 tests minimum**","status":"closed","priority":1,"issue_type":"feature","owner":"jsightler@example.com","created_at":"2026-02-15T21:59:25Z","created_by":"jsightler","updated_at":"2026-02-15T22:04:43Z","closed_at":"2026-02-15T17:04:43Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ha-pecron-buf","title":"Add comprehensive unit tests for token refresh scenarios","description":"Create thorough unit tests to verify token refresh works correctly in all scenarios.\n\n## Test Coverage Needed\n\n### Token Expiration Scenarios\n1. **During initial device list fetch** (get_devices())\n - Verify API reset and retry\n - Confirm fresh login on retry\n - Ensure devices are fetched successfully after refresh\n\n2. **During property fetch** (get_device_properties())\n - Verify API reset when 5032 error occurs\n - Confirm retry logic triggers\n - Ensure successful property fetch after refresh\n\n3. **During TSL fetch** (get_product_tsl())\n - Test auth failure handling\n - Verify graceful fallback if TSL unavailable\n\n4. **During switch control** (set_ac_output/set_dc_output)\n - Test token expiration during control commands\n - Verify user gets appropriate error notification\n\n5. **During service calls** (set_device_property)\n - Test auth failure in service handler\n - Verify proper error reporting\n\n### Error Detection Tests\n- Test all error string patterns: '5032', 'token', 'authentication', '401', 'unauthorized'\n- Verify case-insensitive matching\n- Test with various error message formats from API\n\n### Retry Logic Tests\n- Verify max_retries is respected (2 attempts)\n- Test exponential backoff doesn't apply to auth errors (immediate retry)\n- Confirm non-auth errors don't trigger token refresh\n- Test mixed scenarios (auth error on first device, success on second)\n\n### Edge Cases\n- Multiple simultaneous update cycles with expired token\n- Token expiring between coordinator updates\n- Auth failure followed by connection error\n- Partial success (some devices succeed, one fails auth)\n\n### Mock Structure\n- Mock PecronAPI methods (login, get_devices, get_device_properties, etc.)\n- Simulate various API error responses\n- Track API reset calls (api = None)\n- Verify login() called on retry\n\n## Implementation\n- Use pytest with pytest-asyncio\n- Create test file: tests/test_token_refresh.py\n- Mock coordinator and API interactions\n- Use fixtures for common setup\n- Test both sync (_fetch_data) and async (_async_update_data) layers\n\n## Success Criteria\n- All token refresh scenarios tested\n- Code coverage \u003e 90% for coordinator retry logic\n- Tests pass consistently\n- Clear test names describing each scenario","status":"closed","priority":1,"issue_type":"task","owner":"jsightler@example.com","created_at":"2026-02-14T16:01:26Z","created_by":"jsightler","updated_at":"2026-02-14T16:08:12Z","closed_at":"2026-02-14T11:08:12Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f9dc7..92247ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/custom_components/pecron/__init__.py b/custom_components/pecron/__init__.py index 039f03d..5ae1b2c 100644 --- a/custom_components/pecron/__init__.py +++ b/custom_components/pecron/__init__.py @@ -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 @@ -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 @@ -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", @@ -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", @@ -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", @@ -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}", @@ -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", @@ -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}", @@ -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( @@ -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 @@ -349,7 +361,10 @@ 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)) @@ -357,7 +372,7 @@ def _fetch_data(self) -> dict: _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: @@ -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, @@ -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.""" diff --git a/custom_components/pecron/select.py b/custom_components/pecron/select.py index 7fe2e9a..1652238 100644 --- a/custom_components/pecron/select.py +++ b/custom_components/pecron/select.py @@ -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 @@ -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}", @@ -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( @@ -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", @@ -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}", diff --git a/custom_components/pecron/switch.py b/custom_components/pecron/switch.py index 319696c..6fcdd15 100644 --- a/custom_components/pecron/switch.py +++ b/custom_components/pecron/switch.py @@ -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, @@ -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, @@ -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}", @@ -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( @@ -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", @@ -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}", From c13bc9c2724ad73bb9f00e808ccfdd781c75485a Mon Sep 17 00:00:00 2001 From: jsightler Date: Thu, 30 Jul 2026 19:19:11 -0400 Subject: [PATCH 2/2] sync beads issues.jsonl with main --- .beads/issues.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index c8a7a7b..c92a7cf 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -7,7 +7,7 @@ {"id":"ha-pecron-85t","title":"Implement automatic token refresh on authentication failure","description":"Currently the integration only logs in once during initialization (line 162 in __init__.py). If the Pecron API token expires during normal operation, subsequent API calls fail with authentication errors and the integration stops working until manually reloaded.\n\nWhen an authentication error is detected in _async_update_data (lines 147-149), we should:\n\n1. Reset the API instance to None to trigger re-initialization\n2. Retry the _fetch_data operation which will automatically re-login\n3. Add retry logic with a maximum attempt count to avoid infinite loops\n4. Log the token refresh for debugging\n\nImplementation approach:\n- In _async_update_data, catch authentication errors\n- Set self.api = None to force re-initialization on next attempt\n- Use a retry mechanism (max 2 retries) to re-attempt the data fetch\n- On successful re-login, log 'Token refreshed successfully'\n- If re-login fails after retries, raise UpdateFailed as before\n\nFiles to modify:\n- custom_components/pecron/__init__.py (PecronDataUpdateCoordinator class)\n\nThis is a P0 bug because users will experience integration failures without understanding why, requiring manual intervention to restore functionality.","status":"closed","priority":0,"issue_type":"bug","owner":"jsightler@example.com","created_at":"2026-02-13T20:55:55Z","created_by":"jsightler","updated_at":"2026-02-13T20:57:07Z","closed_at":"2026-02-13T15:57:07Z","close_reason":"Implemented automatic token refresh with retry logic. Integration now handles expired tokens gracefully by resetting API connection and re-logging in automatically.","dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"ha-pecron-env","title":"Fix empty dict falsy check preventing entity creation","description":"Critical bug: In sensor.py line 106 and binary_sensor.py line 81, the check 'if coordinator.data:' treats an empty dict {} as falsy. This means if the API returns no devices or all property fetches fail, coordinator.data will be {} and no entities will be created at all. This is likely why the user sees no power station entities. Fix: Change to 'if coordinator.data is not None:' or 'if coordinator.data:' with explicit empty dict handling.","status":"closed","priority":0,"issue_type":"bug","owner":"jsightler@example.com","created_at":"2026-02-10T05:08:23Z","created_by":"jsightler","updated_at":"2026-02-10T05:12:06Z","closed_at":"2026-02-10T00:12:06Z","close_reason":"Fixed falsy check in sensor.py and binary_sensor.py. Changed from 'if coordinator.data:' to 'if coordinator.data is not None:' to properly handle empty dict. Added warning logs when no devices found.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ha-pecron-nst","title":"[GH#6] Cannot login, error \"cannot_connect\"","description":"## GitHub Issue\nhttps://github.com/jsight/ha-pecron/issues/6\n\n## Summary\nAfter HACS install, login fails almost immediately with error `cannot_connect`. Credentials work in the Android Pecron app.\n\n## Reporter\n@simifor / Simon Hurtado (2026-04-20)\n\n## Suspected cause\nUser may be outside the regions covered by the current endpoint options (only 3 region choices). May need additional API endpoints or better diagnostics for region/connectivity failures.\n\n## Acceptance criteria\n- [ ] Improve logging/diagnostics around cannot_connect during config flow\n- [ ] Confirm which API endpoints/regions are supported and whether more are needed\n- [ ] Fix or document region selection so valid credentials can log in from more locations\n- [ ] Surface a clearer error when the failure is endpoint/region related vs bad credentials","status":"open","priority":1,"issue_type":"bug","owner":"jsightler@example.com","created_at":"2026-07-30T22:55:52Z","created_by":"jsightler","updated_at":"2026-07-30T22:55:52Z","external_ref":"gh-6","labels":["auth","connectivity","github","region"],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ha-pecron-bn4","title":"[GH#8] API error 5014/5041: No permission when fetching properties","description":"## GitHub Issue\nhttps://github.com/jsight/ha-pecron/issues/8\n\n## Summary\nUser can add the Pecron integration and enter credentials, but property fetch fails with:\n`Error fetching properties for F5000LFP_32A8 (key: 5037CD3A3DA8): API error 5014: No permission`\n\nTitle says 5041; log body reports 5014 — both indicate a permission/auth problem after login.\n\n## Reporter\n@kf9ug (2026-07-15)\n\n## Device\nF5000LFP_32A8 (key: 5037CD3A3DA8)\n\n## Steps tried by reporter\n- Verified login/password match Pecron app\n- Changed password on app and re-entered in HA\n- Deleted and re-added credentials\n- Restarted Home Assistant\n\n## Acceptance criteria\n- [ ] Reproduce or identify root cause of API error 5014/5041 No permission\n- [ ] Fix auth/token/permission handling so property fetch works for valid credentials\n- [ ] Surface a clear user-facing error if the account truly lacks device permission\n- [ ] Verify with F5000LFP or equivalent device if possible","notes":"Root cause found: hass.components.persistent_notification was removed from modern Home Assistant core. Every notification call site in __init__.py/switch.py/select.py used the old accessor, so any path that tried to show a notification (no devices found, connection failure, etc.) raised AttributeError and aborted config entry setup with an unrelated traceback -- masking the real 5014/5041 permission message and crashing setup entirely. Fixed by switching to homeassistant.components.persistent_notification.async_create(hass, ...). This does not change the underlying Pecron cloud permission error itself (that's server-side/account-side), but it stops setup from crashing and lets the real error message reach the user/log.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"jsightler","owner":"jsightler@example.com","created_at":"2026-07-30T22:55:49Z","created_by":"jsightler","updated_at":"2026-07-30T23:10:47Z","started_at":"2026-07-30T23:10:45Z","external_ref":"gh-8","labels":["api","auth","github"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ha-pecron-bn4","title":"[GH#8] API error 5014/5041: No permission when fetching properties","description":"## GitHub Issue\nhttps://github.com/jsight/ha-pecron/issues/8\n\n## Summary\nUser can add the Pecron integration and enter credentials, but property fetch fails with:\n`Error fetching properties for F5000LFP_32A8 (key: 5037CD3A3DA8): API error 5014: No permission`\n\nTitle says 5041; log body reports 5014 — both indicate a permission/auth problem after login.\n\n## Reporter\n@kf9ug (2026-07-15)\n\n## Device\nF5000LFP_32A8 (key: 5037CD3A3DA8)\n\n## Steps tried by reporter\n- Verified login/password match Pecron app\n- Changed password on app and re-entered in HA\n- Deleted and re-added credentials\n- Restarted Home Assistant\n\n## Acceptance criteria\n- [ ] Reproduce or identify root cause of API error 5014/5041 No permission\n- [ ] Fix auth/token/permission handling so property fetch works for valid credentials\n- [ ] Surface a clear user-facing error if the account truly lacks device permission\n- [ ] Verify with F5000LFP or equivalent device if possible","notes":"Opened PR #10 (branch fix/persistent-notification-crash) fixing the hass.components.persistent_notification crash. Underlying cloud-side 5014/5041 permission cause still needs the reporter's account to fully diagnose -- keeping issue open/in_progress pending PR merge and reporter feedback.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"jsightler","owner":"jsightler@example.com","created_at":"2026-07-30T22:55:49Z","created_by":"jsightler","updated_at":"2026-07-30T23:14:48Z","started_at":"2026-07-30T23:10:45Z","external_ref":"gh-8","labels":["api","auth","github"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ha-pecron-qcn","title":"Release v0.3.2: Smart time sensor logic","description":"Release with intelligent time sensor state machine.\n\n## New Features\n- Smart Time to Full / Time to Empty sensors\n- State-based display logic (charging/discharging/idle/UPS)\n- Always show both sensors with N/A when not applicable\n- Comprehensive edge case handling\n\n## Testing\n- 18 new unit tests\n- All 43 tests passing","status":"closed","priority":1,"issue_type":"task","owner":"jsightler@example.com","created_at":"2026-02-15T22:06:33Z","created_by":"jsightler","updated_at":"2026-02-15T22:08:09Z","closed_at":"2026-02-15T17:08:09Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ha-pecron-afa","title":"Smart time-remaining sensor logic with charging/discharging state","description":"Improve time-remaining sensors with comprehensive state machine logic and testing.\n\n## State Diagram: Time Sensor Display Logic\n\n### Device States (based on power flow)\n\n1. **IDLE**: input_power == 0 AND output_power == 0\n2. **CHARGING_ONLY**: input_power \u003e 0 AND output_power == 0\n3. **DISCHARGING_ONLY**: input_power == 0 AND output_power \u003e 0\n4. **UPS_MODE**: input_power \u003e 0 AND output_power \u003e 0 (pass-through + battery ready)\n\n### Property Availability Matrix\n\nAPI can return:\n- Both remain_charging_time AND remain_discharging_time\n- Only remain_charging_time (remain_discharging_time missing)\n- Only remain_discharging_time (remain_charging_time missing)\n- Neither property present\n- Property present but value is None/0\n- Property present but value is negative (error condition)\n\n### Display Logic: Time to Full (remain_charging_time)\n\n| State | Property Available | Property Missing | Property = None/0 | Property \u003c 0 |\n|-------|-------------------|------------------|-------------------|--------------|\n| IDLE | N/A | N/A | N/A | N/A |\n| CHARGING_ONLY | Show value | N/A | N/A (or show 0?) | N/A |\n| DISCHARGING_ONLY | N/A | N/A | N/A | N/A |\n| UPS_MODE | Show value | N/A | N/A (or show 0?) | N/A |\n\n### Display Logic: Time to Empty (remain_discharging_time)\n\n| State | Property Available | Property Missing | Property = None/0 | Property \u003c 0 |\n|-------|-------------------|------------------|-------------------|--------------|\n| IDLE | N/A | N/A | N/A | N/A |\n| CHARGING_ONLY | N/A | N/A | N/A | N/A |\n| DISCHARGING_ONLY | Show value | N/A | N/A (or show 0?) | N/A |\n| UPS_MODE | Show value | N/A | N/A (or show 0?) | N/A |\n\n### Edge Cases to Handle\n\n1. **Power value missing**: If total_input_power or total_output_power is None\n - Treat as 0 (use coalescing)\n\n2. **Negative power values**: Device reports -5W sometimes\n - Treat as 0 (use max(0, value))\n\n3. **Small power values**: Measurement noise (1W, 2W)\n - Still use \u003e 0 threshold as user requested\n\n4. **Battery at 100% while charging**: remain_charging_time might be 0\n - Show 0 (not N/A) - user can see charging is complete\n\n5. **Battery fully depleted**: remain_discharging_time might be 0\n - Show 0 (not N/A) - battery is empty\n\n6. **Alternative property names**: \n - remain_time vs remain_discharging_time\n - Check both variants\n\n7. **Both time values show same number**: API bug?\n - Still apply state logic, don't try to detect this\n\n8. **Rapid state transitions**: Device switches between charging/discharging\n - Each update applies current state logic\n - No hysteresis needed\n\n## Implementation Requirements\n\n1. **Always create both sensors** - bypass TSL filtering\n2. **Smart state detection** from input/output power\n3. **Robust property access** - handle missing/None/negative\n4. **Clear logging** - log when returning N/A due to state\n5. **Unit tests** covering all state combinations\n\n## Test Coverage Needed\n\n- All 4 device states × both sensors = 8 core tests\n- Property missing scenarios = 4 tests \n- Property None/0 scenarios = 4 tests\n- Property negative scenarios = 2 tests\n- Power value missing/None = 4 tests\n- Edge cases (100% battery, 0% battery) = 4 tests\n- **Total: ~26 tests minimum**","status":"closed","priority":1,"issue_type":"feature","owner":"jsightler@example.com","created_at":"2026-02-15T21:59:25Z","created_by":"jsightler","updated_at":"2026-02-15T22:04:43Z","closed_at":"2026-02-15T17:04:43Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ha-pecron-buf","title":"Add comprehensive unit tests for token refresh scenarios","description":"Create thorough unit tests to verify token refresh works correctly in all scenarios.\n\n## Test Coverage Needed\n\n### Token Expiration Scenarios\n1. **During initial device list fetch** (get_devices())\n - Verify API reset and retry\n - Confirm fresh login on retry\n - Ensure devices are fetched successfully after refresh\n\n2. **During property fetch** (get_device_properties())\n - Verify API reset when 5032 error occurs\n - Confirm retry logic triggers\n - Ensure successful property fetch after refresh\n\n3. **During TSL fetch** (get_product_tsl())\n - Test auth failure handling\n - Verify graceful fallback if TSL unavailable\n\n4. **During switch control** (set_ac_output/set_dc_output)\n - Test token expiration during control commands\n - Verify user gets appropriate error notification\n\n5. **During service calls** (set_device_property)\n - Test auth failure in service handler\n - Verify proper error reporting\n\n### Error Detection Tests\n- Test all error string patterns: '5032', 'token', 'authentication', '401', 'unauthorized'\n- Verify case-insensitive matching\n- Test with various error message formats from API\n\n### Retry Logic Tests\n- Verify max_retries is respected (2 attempts)\n- Test exponential backoff doesn't apply to auth errors (immediate retry)\n- Confirm non-auth errors don't trigger token refresh\n- Test mixed scenarios (auth error on first device, success on second)\n\n### Edge Cases\n- Multiple simultaneous update cycles with expired token\n- Token expiring between coordinator updates\n- Auth failure followed by connection error\n- Partial success (some devices succeed, one fails auth)\n\n### Mock Structure\n- Mock PecronAPI methods (login, get_devices, get_device_properties, etc.)\n- Simulate various API error responses\n- Track API reset calls (api = None)\n- Verify login() called on retry\n\n## Implementation\n- Use pytest with pytest-asyncio\n- Create test file: tests/test_token_refresh.py\n- Mock coordinator and API interactions\n- Use fixtures for common setup\n- Test both sync (_fetch_data) and async (_async_update_data) layers\n\n## Success Criteria\n- All token refresh scenarios tested\n- Code coverage \u003e 90% for coordinator retry logic\n- Tests pass consistently\n- Clear test names describing each scenario","status":"closed","priority":1,"issue_type":"task","owner":"jsightler@example.com","created_at":"2026-02-14T16:01:26Z","created_by":"jsightler","updated_at":"2026-02-14T16:08:12Z","closed_at":"2026-02-14T11:08:12Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}