diff --git a/custom_components/hon/__init__.py b/custom_components/hon/__init__.py index 1e38a2d..b4b2ed6 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,114 @@ CONFIG_SCHEMA = vol.Schema( ) -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: + # 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: + _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 _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(msg: Any) -> None: + """Handle updates from MQTT subscription in a thread-safe way.""" + 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 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 + 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( 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.""" + try: + hon = hass.data[DOMAIN][entry.unique_id]["hon"] - 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 + # 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 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 + except Exception as exc: + _LOGGER.error("Error unloading entry: %s", exc) + return False \ No newline at end of file diff --git a/custom_components/hon/binary_sensor.py b/custom_components/hon/binary_sensor.py index 3907325..a7f3114 100644 --- a/custom_components/hon/binary_sensor.py +++ b/custom_components/hon/binary_sensor.py @@ -7,9 +7,8 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN from .entity import HonEntity @@ -317,7 +316,7 @@ BINARY_SENSORS["WD"] = unique_entities(BINARY_SENSORS["WM"], BINARY_SENSORS["TD" async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] for device in hass.data[DOMAIN][entry.unique_id]["hon"].appliances: diff --git a/custom_components/hon/button.py b/custom_components/hon/button.py index ce0f548..4a30ef8 100644 --- a/custom_components/hon/button.py +++ b/custom_components/hon/button.py @@ -6,7 +6,7 @@ from homeassistant.components.button import ButtonEntityDescription, ButtonEntit from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from pyhon.appliance import HonAppliance from .const import DOMAIN @@ -56,7 +56,7 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities: list[HonButtonType] = [] for device in hass.data[DOMAIN][entry.unique_id]["hon"].appliances: @@ -88,7 +88,7 @@ class HonButtonEntity(HonEntity, ButtonEntity): class HonDeviceInfo(HonEntity, ButtonEntity): def __init__( - self, hass: HomeAssistantType, entry: ConfigEntry, device: HonAppliance + self, hass: HomeAssistant, entry: ConfigEntry, device: HonAppliance ) -> None: super().__init__(hass, entry, device) @@ -108,7 +108,7 @@ class HonDeviceInfo(HonEntity, ButtonEntity): class HonDataArchive(HonEntity, ButtonEntity): def __init__( - self, hass: HomeAssistantType, entry: ConfigEntry, device: HonAppliance + self, hass: HomeAssistant, entry: ConfigEntry, device: HonAppliance ) -> None: super().__init__(hass, entry, device) diff --git a/custom_components/hon/climate.py b/custom_components/hon/climate.py index f3ce937..ebcc2eb 100644 --- a/custom_components/hon/climate.py +++ b/custom_components/hon/climate.py @@ -19,9 +19,8 @@ from homeassistant.const import ( ATTR_TEMPERATURE, UnitOfTemperature, ) -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from pyhon.appliance import HonAppliance from pyhon.parameter.range import HonParameterRange @@ -104,7 +103,7 @@ CLIMATES: dict[ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] entity: HonClimateEntity | HonACClimateEntity @@ -130,7 +129,7 @@ class HonACClimateEntity(HonEntity, ClimateEntity): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, device: HonAppliance, description: HonACClimateEntityDescription, @@ -199,7 +198,7 @@ class HonACClimateEntity(HonEntity, ClimateEntity): self._attr_hvac_mode = hvac_mode if hvac_mode == HVACMode.OFF: await self._device.commands["stopProgram"].send() - self._device.sync_command("stopProgram", "settings") + self._device.settings["settings.onOffStatus"].value = "2" else: self._device.settings["settings.onOffStatus"].value = "1" setting = self._device.settings["settings.machMode"] @@ -217,6 +216,10 @@ class HonACClimateEntity(HonEntity, ClimateEntity): self._device.sync_command("startProgram", "settings") async def async_turn_off(self, **kwargs: Any) -> None: + fix_param = self._device.commands["stopProgram"].parameters.get("windDirectionVerticalPositionSequence") + if fix_param and fix_param.value == "0": + fix_param.value = "2" + _LOGGER.warning("🔧 Patched 'windDirectionVerticalPositionSequence' from '0' to '2'") await self._device.commands["stopProgram"].send() self._device.sync_command("stopProgram", "settings") @@ -282,7 +285,7 @@ class HonACClimateEntity(HonEntity, ClimateEntity): if swing_mode in [SWING_OFF, SWING_HORIZONTAL] and vertical.value == "8": vertical.value = "5" if swing_mode in [SWING_OFF, SWING_VERTICAL] and horizontal.value == "7": - horizontal.value = "0" + horizontal.value = "2" self._attr_swing_mode = swing_mode await self._device.commands["settings"].send() self.async_write_ha_state() @@ -299,7 +302,7 @@ class HonClimateEntity(HonEntity, ClimateEntity): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, device: HonAppliance, description: HonClimateEntityDescription, diff --git a/custom_components/hon/entity.py b/custom_components/hon/entity.py index a597052..1e339c5 100644 --- a/custom_components/hon/entity.py +++ b/custom_components/hon/entity.py @@ -1,9 +1,8 @@ from typing import Optional, Any from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) @@ -20,7 +19,7 @@ class HonEntity(CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]]): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, device: HonAppliance, description: Optional[HonEntityDescription] = None, diff --git a/custom_components/hon/fan.py b/custom_components/hon/fan.py index a07a08f..9970861 100644 --- a/custom_components/hon/fan.py +++ b/custom_components/hon/fan.py @@ -8,9 +8,8 @@ from homeassistant.components.fan import ( FanEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -36,7 +35,7 @@ FANS: dict[str, tuple[FanEntityDescription, ...]] = { async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] for device in hass.data[DOMAIN][entry.unique_id]["hon"].appliances: @@ -56,7 +55,7 @@ class HonFanEntity(HonEntity, FanEntity): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, device: HonAppliance, description: FanEntityDescription, diff --git a/custom_components/hon/light.py b/custom_components/hon/light.py index d38a994..ce1307e 100644 --- a/custom_components/hon/light.py +++ b/custom_components/hon/light.py @@ -8,9 +8,8 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from pyhon.appliance import HonAppliance from pyhon.parameter.range import HonParameterRange @@ -53,7 +52,7 @@ LIGHTS: dict[str, tuple[LightEntityDescription, ...]] = { async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] for device in hass.data[DOMAIN][entry.unique_id]["hon"].appliances: @@ -73,7 +72,7 @@ class HonLightEntity(HonEntity, LightEntity): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, device: HonAppliance, description: LightEntityDescription, diff --git a/custom_components/hon/lock.py b/custom_components/hon/lock.py index 1ecb744..d85364f 100644 --- a/custom_components/hon/lock.py +++ b/custom_components/hon/lock.py @@ -3,9 +3,8 @@ from typing import Any from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from pyhon.parameter.base import HonParameter from pyhon.parameter.range import HonParameterRange @@ -26,7 +25,7 @@ LOCKS: dict[str, tuple[LockEntityDescription, ...]] = { async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] for device in hass.data[DOMAIN][entry.unique_id]["hon"].appliances: diff --git a/custom_components/hon/manifest.json b/custom_components/hon/manifest.json index e0f00e4..c70766b 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" +} \ No newline at end of file diff --git a/custom_components/hon/number.py b/custom_components/hon/number.py index b646b75..83f96d3 100644 --- a/custom_components/hon/number.py +++ b/custom_components/hon/number.py @@ -8,10 +8,9 @@ from homeassistant.components.number import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTime, UnitOfTemperature -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from pyhon.appliance import HonAppliance from pyhon.parameter.range import HonParameterRange @@ -207,7 +206,7 @@ NUMBERS["WD"] = unique_entities(NUMBERS["WM"], NUMBERS["TD"]) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] entity: HonNumberEntity | HonConfigNumberEntity @@ -230,7 +229,7 @@ class HonNumberEntity(HonEntity, NumberEntity): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, device: HonAppliance, description: HonNumberEntityDescription, @@ -285,7 +284,7 @@ class HonConfigNumberEntity(HonEntity, NumberEntity): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, device: HonAppliance, description: HonConfigNumberEntityDescription, diff --git a/custom_components/hon/select.py b/custom_components/hon/select.py index c7da326..29c19fe 100644 --- a/custom_components/hon/select.py +++ b/custom_components/hon/select.py @@ -6,10 +6,9 @@ from dataclasses import dataclass from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature, UnitOfTime, REVOLUTIONS_PER_MINUTE -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from . import const from .const import DOMAIN @@ -211,7 +210,7 @@ SELECTS["WD"] = unique_entities(SELECTS["WM"], SELECTS["TD"]) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] entity: HonSelectEntity | HonConfigSelectEntity diff --git a/custom_components/hon/sensor.py b/custom_components/hon/sensor.py index 4039282..9731050 100644 --- a/custom_components/hon/sensor.py +++ b/custom_components/hon/sensor.py @@ -21,10 +21,9 @@ from homeassistant.const import ( UnitOfTime, UnitOfTemperature, ) -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from . import const from .const import DOMAIN @@ -808,7 +807,7 @@ SENSORS["WD"] = unique_entities(SENSORS["WM"], SENSORS["TD"]) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] entity: HonSensorEntity | HonConfigSensorEntity @@ -834,6 +833,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 +844,8 @@ class HonSensorEntity(HonEntity, SensorEntity): 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 +855,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: @@ -874,4 +876,4 @@ class HonConfigSensorEntity(HonEntity, SensorEntity): value = get_readable(self.entity_description, value) self._attr_native_value = value if update: - self.async_write_ha_state() + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/hon/switch.py b/custom_components/hon/switch.py index 359e505..6141fa4 100644 --- a/custom_components/hon/switch.py +++ b/custom_components/hon/switch.py @@ -5,10 +5,9 @@ from typing import Any from homeassistant.components.switch import SwitchEntityDescription, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from pyhon.parameter.base import HonParameter from pyhon.parameter.range import HonParameterRange @@ -403,7 +402,7 @@ SWITCHES["WD"] = unique_entities(SWITCHES["WD"], SWITCHES["TD"]) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] entity: HonConfigSwitchEntity | HonControlSwitchEntity | HonSwitchEntity