diff --git a/custom_components/wundasmart/__init__.py b/custom_components/wundasmart/__init__.py index 54f5ad9..38b56a7 100644 --- a/custom_components/wundasmart/__init__.py +++ b/custom_components/wundasmart/__init__.py @@ -3,17 +3,18 @@ from datetime import timedelta import asyncio +import aiohttp import logging from typing import Final from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.device_registry import DeviceInfo -from .const import DOMAIN +from .const import * from .pywundasmart import get_devices _LOGGER = logging.getLogger(__name__) @@ -31,9 +32,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: wunda_ip = entry.data[CONF_HOST] wunda_user = entry.data[CONF_USERNAME] wunda_pass = entry.data[CONF_PASSWORD] + update_interval = timedelta(seconds=entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) + connect_timeout = entry.data.get(CONF_CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT) + read_timeout = entry.data.get(CONF_READ_TIMEOUT, DEFAULT_READ_TIMEOUT) + timeout = aiohttp.ClientTimeout(sock_connect=connect_timeout, sock_read=read_timeout) coordinator = WundasmartDataUpdateCoordinator( - hass, wunda_ip, wunda_user, wunda_pass + hass, wunda_ip, wunda_user, wunda_pass, update_interval, timeout ) await coordinator.async_config_entry_first_refresh() @@ -69,7 +74,7 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> Non class WundasmartDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching data from WundaSmart API.""" - def __init__(self, hass, wunda_ip, wunda_user, wunda_pass): + def __init__(self, hass, wunda_ip, wunda_user, wunda_pass, update_interval, timeout): """Initialize.""" self._hass = hass self._wunda_ip = wunda_ip @@ -80,13 +85,13 @@ def __init__(self, hass, wunda_ip, wunda_user, wunda_pass): self._device_name = None self._sw_version = None self._hw_version = None + self._timeout = timeout - update_interval = timedelta(minutes=1) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) async def _async_update_data(self): attempts = 0 - max_attempts = 30 + max_attempts = 5 session = aiohttp_client.async_get_clientsession(self._hass) while attempts < max_attempts: attempts += 1 @@ -96,7 +101,7 @@ async def _async_update_data(self): self._wunda_ip, self._wunda_user, self._wunda_pass, - timeout=1 + timeout=self._timeout ) if result["state"]: diff --git a/custom_components/wundasmart/climate.py b/custom_components/wundasmart/climate.py index f62bf88..05ada0d 100644 --- a/custom_components/wundasmart/climate.py +++ b/custom_components/wundasmart/climate.py @@ -3,6 +3,7 @@ import math import logging +import aiohttp from typing import Any from homeassistant.components.climate import ( @@ -29,7 +30,7 @@ from . import WundasmartDataUpdateCoordinator from .pywundasmart import send_command -from .const import DOMAIN, MIN_ROOM_ID, MIN_TRV_ID, MAX_TRV_ID +from .const import * _LOGGER = logging.getLogger(__name__) @@ -65,6 +66,11 @@ async def async_setup_entry( wunda_pass: str = entry.data[CONF_PASSWORD] coordinator: WundasmartDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] session = aiohttp_client.async_get_clientsession(hass) + + connect_timeout = entry.data.get(CONF_CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT) + read_timeout = entry.data.get(CONF_READ_TIMEOUT, DEFAULT_READ_TIMEOUT) + timeout = aiohttp.ClientTimeout(sock_connect=connect_timeout, sock_read=read_timeout) + rooms = ( (wunda_id, device) for wunda_id, device in coordinator.data.items() @@ -78,6 +84,7 @@ async def async_setup_entry( wunda_id, device, coordinator, + timeout ) for wunda_id, device in rooms)) @@ -99,6 +106,7 @@ def __init__( wunda_id: str, device: dict[str, Any], coordinator: WundasmartDataUpdateCoordinator, + timeout: aiohttp.ClientTimeout ) -> None: """Initialize the Wundasmart climate.""" super().__init__(coordinator) @@ -119,6 +127,7 @@ def __init__( self._attr_current_humidity = None self._attr_hvac_mode = HVACMode.AUTO self._attr_preset_mode = None + self._timeout = timeout # Update with initial state self.__update_state() @@ -277,13 +286,19 @@ async def async_added_to_hass(self) -> None: async def async_set_temperature(self, temperature, **kwargs): # Set the new target temperature - await send_command(self._session, self._wunda_ip, self._wunda_user, self._wunda_pass, params={ - "cmd": 1, - "roomid": self._wunda_id, - "temp": temperature, - "locktt": 0, - "time": 0 - }) + await send_command( + self._session, + self._wunda_ip, + self._wunda_user, + self._wunda_pass, + timeout=self._timeout, + params={ + "cmd": 1, + "roomid": self._wunda_id, + "temp": temperature, + "locktt": 0, + "time": 0 + }) # Fetch the updated state await self.coordinator.async_request_refresh() @@ -291,31 +306,49 @@ async def async_set_temperature(self, temperature, **kwargs): async def async_set_hvac_mode(self, hvac_mode: HVACMode): if hvac_mode == HVACMode.AUTO: # Set to programmed mode - await send_command(self._session, self._wunda_ip, self._wunda_user, self._wunda_pass, params={ - "cmd": 1, - "roomid": self._wunda_id, - "prog": None, - "locktt": 0, - "time": 0 - }) + await send_command( + self._session, + self._wunda_ip, + self._wunda_user, + self._wunda_pass, + timeout=self._timeout, + params={ + "cmd": 1, + "roomid": self._wunda_id, + "prog": None, + "locktt": 0, + "time": 0 + }) elif hvac_mode == HVACMode.HEAT: # Set the target temperature to the current temperature + 1 degree, rounded up - await send_command(self._session, self._wunda_ip, self._wunda_user, self._wunda_pass, params={ - "cmd": 1, - "roomid": self._wunda_id, - "temp": math.ceil(self._attr_current_temperature) + 1, - "locktt": 0, - "time": 0 - }) + await send_command( + self._session, + self._wunda_ip, + self._wunda_user, + self._wunda_pass, + timeout=self._timeout, + params={ + "cmd": 1, + "roomid": self._wunda_id, + "temp": math.ceil(self._attr_current_temperature) + 1, + "locktt": 0, + "time": 0 + }) elif hvac_mode == HVACMode.OFF: # Set the target temperature to zero - await send_command(self._session, self._wunda_ip, self._wunda_user, self._wunda_pass, params={ - "cmd": 1, - "roomid": self._wunda_id, - "temp": 0.0, - "locktt": 0, - "time": 0 - }) + await send_command( + self._session, + self._wunda_ip, + self._wunda_user, + self._wunda_pass, + timeout=self._timeout, + params={ + "cmd": 1, + "roomid": self._wunda_id, + "temp": 0.0, + "locktt": 0, + "time": 0 + }) else: raise NotImplementedError(f"Unsupported HVAC mode {hvac_mode}") @@ -335,6 +368,7 @@ async def async_set_preset_mode(self, preset_mode) -> None: self._wunda_ip, self._wunda_user, self._wunda_pass, + timeout=self._timeout, params={ "cmd": 1, "roomid": self._wunda_id, diff --git a/custom_components/wundasmart/config_flow.py b/custom_components/wundasmart/config_flow.py index d914e5b..c965ae6 100644 --- a/custom_components/wundasmart/config_flow.py +++ b/custom_components/wundasmart/config_flow.py @@ -1,11 +1,14 @@ """Config flow to configure Wundasmart.""" import voluptuous as vol +from typing import Any from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client +from homeassistant.core import callback -from .const import DOMAIN +from .const import * from .pywundasmart import get_devices STEP_USER_DATA_SCHEMA = vol.Schema( @@ -16,6 +19,14 @@ } ) +STEP_INIT_DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int, + vol.Optional(CONF_CONNECT_TIMEOUT, default=DEFAULT_CONNECT_TIMEOUT): int, + vol.Optional(CONF_READ_TIMEOUT, default=DEFAULT_READ_TIMEOUT): int + } +) + class Hub: """Wundasmart Hub class.""" @@ -53,6 +64,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + @staticmethod + @callback + def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + return OptionsFlow(config_entry) + async def async_step_user(self, user_input=None): """Show the setup form to the user.""" if user_input is None: @@ -81,6 +98,18 @@ async def async_step_user(self, user_input=None): ) +class OptionsFlow(config_entries.OptionsFlow): + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + self.config_entry = config_entry + + async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult: + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form(step_id="init", data_schema=STEP_INIT_DATA_SCHEMA) + + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/custom_components/wundasmart/const.py b/custom_components/wundasmart/const.py index e8f70ee..15f6b2f 100644 --- a/custom_components/wundasmart/const.py +++ b/custom_components/wundasmart/const.py @@ -2,6 +2,13 @@ DOMAIN = "wundasmart" +CONF_CONNECT_TIMEOUT = "connect_timeout" +CONF_READ_TIMEOUT = "read_timeout" + +DEFAULT_SCAN_INTERVAL = 300 +DEFAULT_CONNECT_TIMEOUT = 5 +DEFAULT_READ_TIMEOUT = 5 + MIN_SENSOR_ID = 1 MAX_SENSOR_ID = 30 MIN_TRV_ID = 31 diff --git a/custom_components/wundasmart/strings.json b/custom_components/wundasmart/strings.json index 2a4b622..2456fb6 100644 --- a/custom_components/wundasmart/strings.json +++ b/custom_components/wundasmart/strings.json @@ -20,6 +20,18 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "[%key:common::config_flow::data::scan_interval%]", + "connect_timeout": "Connect Timeout", + "read_timeout": "Read Timeout" + }, + "title": "Wundasmart options" + } + } + }, "entity": { "climate": { "wundasmart": { diff --git a/custom_components/wundasmart/translations/en.json b/custom_components/wundasmart/translations/en.json index 1f4d38e..64b4681 100644 --- a/custom_components/wundasmart/translations/en.json +++ b/custom_components/wundasmart/translations/en.json @@ -20,6 +20,18 @@ } } }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Poll Interval", + "connect_timeout": "Connect Timeout", + "read_timeout": "Read Timeout" + }, + "title": "Wundasmart options" + } + } + }, "entity": { "climate": { "wundasmart": { diff --git a/custom_components/wundasmart/water_heater.py b/custom_components/wundasmart/water_heater.py index dad988a..ca92cd8 100644 --- a/custom_components/wundasmart/water_heater.py +++ b/custom_components/wundasmart/water_heater.py @@ -24,10 +24,11 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.helpers.config_validation as cv import voluptuous as vol +import aiohttp from . import WundasmartDataUpdateCoordinator from .pywundasmart import send_command -from .const import DOMAIN +from .const import * _LOGGER = logging.getLogger(__name__) @@ -81,6 +82,11 @@ async def async_setup_entry( wunda_pass: str = entry.data[CONF_PASSWORD] coordinator: WundasmartDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] session = aiohttp_client.async_get_clientsession(hass) + + connect_timeout = entry.data.get(CONF_CONNECT_TIMEOUT, DEFAULT_CONNECT_TIMEOUT) + read_timeout = entry.data.get(CONF_READ_TIMEOUT, DEFAULT_READ_TIMEOUT) + timeout = aiohttp.ClientTimeout(sock_connect=connect_timeout, sock_read=read_timeout) + async_add_entities( Device( session, @@ -90,6 +96,7 @@ async def async_setup_entry( wunda_id, device, coordinator, + timeout ) for wunda_id, device in coordinator.data.items() if device.get("device_type") == "wunda" and "device_name" in device ) @@ -114,9 +121,6 @@ async def async_setup_entry( ) - - - class Device(CoordinatorEntity[WundasmartDataUpdateCoordinator], WaterHeaterEntity): """Representation of an Wundasmart water heater.""" @@ -134,6 +138,7 @@ def __init__( wunda_id: str, device: dict[str, Any], coordinator: WundasmartDataUpdateCoordinator, + timeout: aiohttp.ClientTimeout ) -> None: """Initialize the Wundasmart water_heater.""" super().__init__(coordinator) @@ -146,6 +151,7 @@ def __init__( self._attr_unique_id = device["id"] self._attr_type = device["device_type"] self._attr_device_info = coordinator.device_info + self._timeout = timeout # Update with initial state self.__update_state() @@ -190,21 +196,39 @@ async def async_set_operation_mode(self, operation_mode: str) -> None: if operation_mode: if operation_mode in HW_OFF_OPERATIONS: _, duration = _split_operation(operation_mode) - await send_command(self._session, self._wunda_ip, self._wunda_user, self._wunda_pass, params={ - "cmd": 3, - "hw_off_time": duration - }) + await send_command( + self._session, + self._wunda_ip, + self._wunda_user, + self._wunda_pass, + timeout=self._timeout, + params={ + "cmd": 3, + "hw_off_time": duration + }) elif operation_mode in HW_BOOST_OPERATIONS: _, duration = _split_operation(operation_mode) - await send_command(self._session, self._wunda_ip, self._wunda_user, self._wunda_pass, params={ - "cmd": 3, - "hw_boost_time": duration - }) + await send_command( + self._session, + self._wunda_ip, + self._wunda_user, + self._wunda_pass, + timeout=self._timeout, + params={ + "cmd": 3, + "hw_boost_time": duration + }) elif operation_mode == OPERATION_SET_AUTO: - await send_command(self._session, self._wunda_ip, self._wunda_user, self._wunda_pass, params={ - "cmd": 3, - "hw_boost_time": 0 - }) + await send_command( + self._session, + self._wunda_ip, + self._wunda_user, + self._wunda_pass, + timeout=self._timeout, + params={ + "cmd": 3, + "hw_boost_time": 0 + }) else: raise NotImplementedError(f"Unsupported operation mode {operation_mode}")