From 1b5cf9eb96485ee2f3b459dc0946588be6aa9e72 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Sun, 29 Dec 2024 10:33:59 +0100 Subject: [PATCH 1/2] fix scheduled update --- custom_components/hon/__init__.py | 105 ++++++++++++++++++++++-------- custom_components/hon/sensor.py | 5 +- 2 files changed, 81 insertions(+), 29 deletions(-) diff --git a/custom_components/hon/__init__.py b/custom_components/hon/__init__.py index 1e38a2d..a7c505e 100644 --- a/custom_components/hon/__init__.py +++ b/custom_components/hon/__init__.py @@ -1,12 +1,15 @@ import logging +from datetime import timedelta from pathlib import Path from typing import Any import voluptuous as vol # type: ignore[import-untyped] + +from homeassistant.core import HomeAssistant, callback from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.helpers import config_validation as cv, aiohttp_client -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from pyhon import Hon @@ -27,47 +30,93 @@ ) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Hon from a config entry.""" session = aiohttp_client.async_get_clientsession(hass) - if (config_dir := hass.config.config_dir) is None: - raise ValueError("Missing Config Dir") - hon = await Hon( - email=entry.data[CONF_EMAIL], - password=entry.data[CONF_PASSWORD], - mobile_id=MOBILE_ID, - session=session, - test_data_path=Path(config_dir), - refresh_token=entry.data.get(CONF_REFRESH_TOKEN, ""), - ).create() + + try: + # Create Hon instance + hon = Hon( + email=entry.data[CONF_EMAIL], + password=entry.data[CONF_PASSWORD], + mobile_id=MOBILE_ID, + session=session, + test_data_path=Path(hass.config.config_dir), + refresh_token=entry.data.get(CONF_REFRESH_TOKEN, ""), + ) + + # Initialize Hon in executor + hon = await hon.create() + + except Exception as exc: + _LOGGER.error("Error creating Hon instance: %s", exc) + raise + + async def async_update_data() -> dict[str, Any]: + """Fetch data from API.""" + try: + for appliance in hon.appliances: + await appliance.update() + return {"last_update": hon.api.auth.refresh_token} + except Exception as exc: + _LOGGER.error("Error updating Hon data: %s", exc) + raise + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=timedelta(seconds=60), + ) + + def _mqtt_update() -> None: + """Handle MQTT update in event loop.""" + coordinator.async_set_updated_data({"last_update": hon.api.auth.refresh_token}) + + def handle_update(_: Any) -> None: + """Handle updates from MQTT subscription in a thread-safe way.""" + hass.loop.call_soon_threadsafe(_mqtt_update) + + # Subscribe to MQTT updates + hon.subscribe_updates(handle_update) + + # Initial data fetch + await coordinator.async_config_entry_first_refresh() # Save the new refresh token hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_REFRESH_TOKEN: hon.api.auth.refresh_token} ) - coordinator: DataUpdateCoordinator[dict[str, Any]] = DataUpdateCoordinator( - hass, _LOGGER, name=DOMAIN - ) - hon.subscribe_updates(coordinator.async_set_updated_data) - hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = {"hon": hon, "coordinator": coordinator} - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: - refresh_token = hass.data[DOMAIN][entry.unique_id]["hon"].api.auth.refresh_token +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + hon = hass.data[DOMAIN][entry.unique_id]["hon"] + # Store refresh token + refresh_token = hon.api.auth.refresh_token + + # Unsubscribe from updates + try: + hon.subscribe_updates(None) # Remove subscription + except Exception as exc: + _LOGGER.warning("Error unsubscribing from updates: %s", exc) + + # Update entry with latest refresh token hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_REFRESH_TOKEN: refresh_token} ) - unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload: - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN, None) - return unload + + # Unload platforms + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.unique_id) + + return unload_ok \ No newline at end of file diff --git a/custom_components/hon/sensor.py b/custom_components/hon/sensor.py index 4039282..55f43eb 100644 --- a/custom_components/hon/sensor.py +++ b/custom_components/hon/sensor.py @@ -834,6 +834,7 @@ class HonSensorEntity(HonEntity, SensorEntity): @callback def _handle_coordinator_update(self, update: bool = True) -> None: + """Handle updated data from the coordinator.""" value = self._device.get(self.entity_description.key, "") if self.entity_description.key == "programName": if not (options := self._device.settings.get("startProgram.program")): @@ -844,7 +845,8 @@ def _handle_coordinator_update(self, update: bool = True) -> None: value = str(get_readable(self.entity_description, value)) if not value and self.entity_description.state_class is not None: self._attr_native_value = 0 - self._attr_native_value = value + else: + self._attr_native_value = value if update: self.async_write_ha_state() @@ -854,6 +856,7 @@ class HonConfigSensorEntity(HonEntity, SensorEntity): @callback def _handle_coordinator_update(self, update: bool = True) -> None: + """Handle updated data from the coordinator.""" sensor = self._device.settings.get(self.entity_description.key, None) value: float | str if self.entity_description.state_class is not None: From ea404bb4773adb6b6e44df80855698a8a6eee498 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Sun, 29 Dec 2024 10:55:29 +0100 Subject: [PATCH 2/2] update manifest --- custom_components/hon/__init__.py | 93 ++++++++++++++++++----------- custom_components/hon/manifest.json | 5 +- 2 files changed, 60 insertions(+), 38 deletions(-) diff --git a/custom_components/hon/__init__.py b/custom_components/hon/__init__.py index a7c505e..ba91354 100644 --- a/custom_components/hon/__init__.py +++ b/custom_components/hon/__init__.py @@ -35,17 +35,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = aiohttp_client.async_get_clientsession(hass) try: - # Create Hon instance - hon = Hon( - email=entry.data[CONF_EMAIL], - password=entry.data[CONF_PASSWORD], - mobile_id=MOBILE_ID, - session=session, - test_data_path=Path(hass.config.config_dir), - refresh_token=entry.data.get(CONF_REFRESH_TOKEN, ""), - ) - - # Initialize Hon in executor + # Initialize Hon instance in executor + def init_hon(): + """Initialize Hon instance.""" + return Hon( + email=entry.data[CONF_EMAIL], + password=entry.data[CONF_PASSWORD], + mobile_id=MOBILE_ID, + session=session, + test_data_path=Path(hass.config.config_dir), + refresh_token=entry.data.get(CONF_REFRESH_TOKEN, ""), + ) + + # Create Hon instance in executor + hon = await hass.async_add_executor_job(init_hon) + # Create and initialize hon = await hon.create() except Exception as exc: @@ -70,19 +74,32 @@ async def async_update_data() -> dict[str, Any]: update_interval=timedelta(seconds=60), ) - def _mqtt_update() -> None: - """Handle MQTT update in event loop.""" - coordinator.async_set_updated_data({"last_update": hon.api.auth.refresh_token}) + def _handle_mqtt_update(_: Any) -> None: + """Handle MQTT updates.""" + try: + coordinator.async_set_updated_data({"last_update": hon.api.auth.refresh_token}) + except Exception as exc: + _LOGGER.error("Error handling MQTT update: %s", exc) - def handle_update(_: Any) -> None: + def handle_update(msg: Any) -> None: """Handle updates from MQTT subscription in a thread-safe way.""" - hass.loop.call_soon_threadsafe(_mqtt_update) + try: + hass.loop.call_soon_threadsafe(_handle_mqtt_update, msg) + except Exception as exc: + _LOGGER.error("Error scheduling MQTT update: %s", exc) - # Subscribe to MQTT updates - hon.subscribe_updates(handle_update) + # Subscribe to MQTT updates with error handling + try: + hon.subscribe_updates(handle_update) + except Exception as exc: + _LOGGER.error("Error subscribing to MQTT updates: %s", exc) # Initial data fetch - await coordinator.async_config_entry_first_refresh() + try: + await coordinator.async_config_entry_first_refresh() + except Exception as exc: + _LOGGER.error("Error during initial refresh: %s", exc) + raise # Save the new refresh token hass.config_entries.async_update_entry( @@ -98,25 +115,29 @@ def handle_update(_: Any) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - hon = hass.data[DOMAIN][entry.unique_id]["hon"] + try: + hon = hass.data[DOMAIN][entry.unique_id]["hon"] - # Store refresh token - refresh_token = hon.api.auth.refresh_token + # Store refresh token + refresh_token = hon.api.auth.refresh_token - # Unsubscribe from updates - try: - hon.subscribe_updates(None) # Remove subscription - except Exception as exc: - _LOGGER.warning("Error unsubscribing from updates: %s", exc) + # Unsubscribe from updates + try: + hon.subscribe_updates(None) # Remove subscription + except Exception as exc: + _LOGGER.warning("Error unsubscribing from updates: %s", exc) - # Update entry with latest refresh token - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_REFRESH_TOKEN: refresh_token} - ) + # Update entry with latest refresh token + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_REFRESH_TOKEN: refresh_token} + ) - # Unload platforms - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.unique_id) + # Unload platforms + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.unique_id) - return unload_ok \ No newline at end of file + return unload_ok + except Exception as exc: + _LOGGER.error("Error unloading entry: %s", exc) + return False diff --git a/custom_components/hon/manifest.json b/custom_components/hon/manifest.json index e0f00e4..ecf6fcc 100644 --- a/custom_components/hon/manifest.json +++ b/custom_components/hon/manifest.json @@ -2,7 +2,8 @@ "domain": "hon", "name": "Haier hOn", "codeowners": [ - "@Andre0512" + "@Andre0512", + "@galvani" ], "config_flow": true, "documentation": "https://github.com/Andre0512/hon/", @@ -11,5 +12,5 @@ "requirements": [ "pyhOn==0.17.5" ], - "version": "0.14.0" + "version": "0.14.1" }