소스 검색

fix: thread-safety, token refresh 404, via_device warning (#1 #2)

Fixes three issues reported by users:

- Issue #1 (HACS structure) was resolved in v2.4.0. Closing with comment.

- Issue #2 — three distinct bugs all contributing to 'Unknown' sensors
  and HA warning logs:

  1. Thread-safety: on_token_refreshed callback called async_update_entry
     from a background executor thread. Wrapped in call_soon_threadsafe()
     to schedule the update onto the HA event loop. Fixes the crash/
     data-corruption warning in HA logs.

  2. Token refresh 404: API_REFRESH_TOKEN endpoint was missing the /apis/
     prefix (/login/... → /apis/login/...). Every refresh silently failed,
     causing tokens to expire and sensors to go Unknown.

  3. via_device non-existent: station hub device was never registered in
     the device registry before platform setup, so all per-device entity
     via_device references pointed to a ghost device. Added explicit
     dev_reg.async_get_or_create() for the station device in
     async_setup_entry before async_forward_entry_setups.

Bumps version to 2.4.1.
Conexo Casa 4 달 전
부모
커밋
36523e5ff8
4개의 변경된 파일과 67개의 추가작업 그리고 18개의 파일을 삭제
  1. 28 0
      CHANGELOG.md
  2. 37 16
      custom_components/solar_of_things/__init__.py
  3. 1 1
      custom_components/solar_of_things/const.py
  4. 1 1
      custom_components/solar_of_things/manifest.json

+ 28 - 0
CHANGELOG.md

@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ---
 
+## [2.4.1] - 2026-05-31
+
+### Fixed
+- **Thread-safety crash** (HA warning: *"calls hass.config_entries.async_update_entry
+  from a thread other than the event loop"*) — the `on_token_refreshed` callback was
+  decorated `@callback` but invoked from a background executor thread during token
+  refresh. `async_update_entry` can only be called from the event loop. Fixed by
+  wrapping the update in a nested `@callback` and scheduling it with
+  `hass.loop.call_soon_threadsafe()`. Resolves crash/data-corruption risk reported
+  in issue #2.
+
+- **Token refresh 404** (*"token refresh request failed: 404 Not Found for url:
+  https://solar.siseli.com/login/refresh/access/token"*) — the refresh endpoint
+  was missing the `/apis/` prefix. Corrected to
+  `/apis/login/refresh/access/token`. This caused every token refresh to fail
+  silently, eventually leading to expired tokens and "Unknown" sensor values.
+  Resolves the sensor data issue reported in issue #2.
+
+- **`via_device` warning** (*"calls device_registry.async_get_or_create referencing
+  a non existing via_device … This will stop working in Home Assistant 2025.12.0"*)
+  — the station hub device was never explicitly registered in the device registry,
+  so per-device entities' `via_device` reference pointed to a non-existent device.
+  The station device is now registered in `async_setup_entry` before
+  `async_forward_entry_setups` is called. Resolves issue #2 comments from Gaz93
+  and andreasantorelli12-hue.
+
+---
+
 ## [2.4.0] - 2026-05-30
 
 ### Added

+ 37 - 16
custom_components/solar_of_things/__init__.py

@@ -31,6 +31,7 @@ from typing import Any
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import Platform
 from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import device_registry as dr
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
 from .api import SolarOfThingsAPI, TokenExpiredError
@@ -69,28 +70,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
     # Build the token-refreshed callback *before* constructing the API so the
     # callback reference is captured.
-    @callback
+    # NOTE: this callback is invoked from a background executor thread (inside
+    # api.login / api.refresh_access_token).  async_update_entry must only be
+    # called from the event loop, so we schedule it with call_soon_threadsafe.
     def _on_token_refreshed(
         access_token: str,
         refresh_token: str,
         access_expires: str,
         refresh_expires: str,
     ) -> None:
-        """Persist refreshed token back to the config entry (non-blocking)."""
-        hass.config_entries.async_update_entry(
-            entry,
-            data={
-                **entry.data,
-                CONF_IOT_TOKEN: access_token,
-                CONF_REFRESH_TOKEN: refresh_token,
-                CONF_ACCESS_TOKEN_EXPIRES: access_expires,
-                CONF_REFRESH_TOKEN_EXPIRES: refresh_expires,
-            },
-        )
-        _LOGGER.debug(
-            "SolarOfThings [%s]: access token refreshed; entry data updated",
-            entry.entry_id,
-        )
+        """Persist refreshed token back to the config entry (thread-safe)."""
+
+        @callback
+        def _update() -> None:
+            hass.config_entries.async_update_entry(
+                entry,
+                data={
+                    **entry.data,
+                    CONF_IOT_TOKEN: access_token,
+                    CONF_REFRESH_TOKEN: refresh_token,
+                    CONF_ACCESS_TOKEN_EXPIRES: access_expires,
+                    CONF_REFRESH_TOKEN_EXPIRES: refresh_expires,
+                },
+            )
+            _LOGGER.debug(
+                "SolarOfThings [%s]: access token refreshed; entry data updated",
+                entry.entry_id,
+            )
+
+        hass.loop.call_soon_threadsafe(_update)
 
     # Instantiate API in the appropriate auth mode
     if user_id and password:
@@ -164,6 +172,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     }
 
     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+
+    # Explicitly register the station hub device so that per-device entities
+    # can reference it via via_device without triggering the
+    # "non existing via_device" warning introduced in HA 2025.12.
+    dev_reg = dr.async_get(hass)
+    dev_reg.async_get_or_create(
+        config_entry_id=entry.entry_id,
+        identifiers={(DOMAIN, station_id)},
+        name=f"Solar Station {station_id}",
+        manufacturer="Siseli",
+        model="Station",
+    )
+
     return True
 
 

+ 1 - 1
custom_components/solar_of_things/const.py

@@ -27,7 +27,7 @@ API_AUTH_BASE_URL   = "https://solar.siseli.com"         # auth / login endpoint
 # ─── Auth endpoints (discovered from portal JS bundle) ─────────────────────────
 # The login endpoint requires IOT-Open-AppID signing (see api.py _sign_request).
 API_LOGIN           = "/apis/login/account"              # POST + signed headers
-API_REFRESH_TOKEN   = "/login/refresh/access/token"      # POST, no token needed
+API_REFRESH_TOKEN   = "/apis/login/refresh/access/token"  # POST, no token needed
 
 # ─── IOT Open Platform app credentials (embedded in portal umi.js) ────────────
 # rBrTRfAPXz is the production AppID accepted by solar.siseli.com.

+ 1 - 1
custom_components/solar_of_things/manifest.json

@@ -11,5 +11,5 @@
   "requirements": [
     "pycryptodome>=3.9.0"
   ],
-  "version": "2.3.3"
+  "version": "2.4.1"
 }