From 5de189287e6699ab53c4d2ee0a0f235598f040de Mon Sep 17 00:00:00 2001 From: AndersonTomas Date: Thu, 8 Jan 2026 13:27:00 +0000 Subject: [PATCH] Upload files to "custom_components/aguaiot" --- custom_components/aguaiot/__init__.py | 49 ++++++++++ custom_components/aguaiot/coordinator.py | 116 +++++++++++++++++++++++ custom_components/aguaiot/diagnostics.py | 33 +++++++ custom_components/aguaiot/manifest.json | 13 +++ custom_components/aguaiot/number.py | 85 +++++++++++++++++ 5 files changed, 296 insertions(+) create mode 100644 custom_components/aguaiot/__init__.py create mode 100644 custom_components/aguaiot/coordinator.py create mode 100644 custom_components/aguaiot/diagnostics.py create mode 100644 custom_components/aguaiot/manifest.json create mode 100644 custom_components/aguaiot/number.py diff --git a/custom_components/aguaiot/__init__.py b/custom_components/aguaiot/__init__.py new file mode 100644 index 0000000..2e1a010 --- /dev/null +++ b/custom_components/aguaiot/__init__.py @@ -0,0 +1,49 @@ +"""Support for Micronova Agua IOT heating devices.""" + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +from .coordinator import AguaIOTDataUpdateCoordinator +from .const import DOMAIN, PLATFORMS + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the AguaIOT integration.""" + if DOMAIN in config: + for entry_config in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up AguaIOT entry.""" + coordinator = AguaIOTDataUpdateCoordinator( + hass=hass, + config_entry=config_entry, + ) + config_entry.runtime_data = coordinator + + await coordinator.async_config_entry_first_refresh() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + # Services + async def async_close_connection(event: Event) -> None: + """Close AguaIOT connection on HA Stop.""" + # await agua.close() + + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) + ) + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/custom_components/aguaiot/coordinator.py b/custom_components/aguaiot/coordinator.py new file mode 100644 index 0000000..060d5c0 --- /dev/null +++ b/custom_components/aguaiot/coordinator.py @@ -0,0 +1,116 @@ +"""Update coordinator""" + +from __future__ import annotations + +import logging +from datetime import timedelta + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers.httpx_client import get_async_client + +from .aguaiot import ( + AguaIOTConnectionError, + AguaIOTError, + AguaIOTUnauthorized, + AguaIOTUpdateError, + aguaiot, +) + +from .const import ( + CONF_API_URL, + CONF_CUSTOMER_CODE, + CONF_LOGIN_API_URL, + CONF_UUID, + CONF_BRAND_ID, + CONF_BRAND, + CONF_LANGUAGE, + CONF_AIR_TEMP_FIX, + CONF_READING_ERROR_FIX, + CONF_UPDATE_INTERVAL, + CONF_HTTP_TIMEOUT, + CONF_BUFFER_READ_TIMEOUT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class AguaIOTDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the API.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + ) -> None: + """Initialize.""" + update_interval = config_entry.options.get(CONF_UPDATE_INTERVAL, 60) + + super().__init__( + hass=hass, + logger=_LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=update_interval), + config_entry=config_entry, + ) + + """Set up AguaIOT entry.""" + api_url = config_entry.data[CONF_API_URL] + customer_code = config_entry.data[CONF_CUSTOMER_CODE] + email = config_entry.data[CONF_EMAIL] + password = config_entry.data[CONF_PASSWORD] + gen_uuid = config_entry.data[CONF_UUID] + login_api_url = config_entry.data.get(CONF_LOGIN_API_URL) + brand_id = config_entry.data.get(CONF_BRAND_ID) + brand = config_entry.data.get(CONF_BRAND) + air_temp_fix = config_entry.options.get(CONF_AIR_TEMP_FIX, False) + reading_error_fix = config_entry.options.get(CONF_READING_ERROR_FIX, False) + language = config_entry.options.get(CONF_LANGUAGE) + http_timeout = config_entry.options.get(CONF_HTTP_TIMEOUT) + buffer_read_timeout = config_entry.options.get(CONF_BUFFER_READ_TIMEOUT) + + self.agua = aguaiot( + api_url=api_url, + customer_code=customer_code, + email=email, + password=password, + unique_id=gen_uuid, + login_api_url=login_api_url, + brand_id=brand_id, + brand=brand, + async_client=get_async_client(hass), + air_temp_fix=air_temp_fix, + reading_error_fix=reading_error_fix, + language=language, + http_timeout=http_timeout, + buffer_read_timeout=buffer_read_timeout, + ) + + async def _async_setup(self) -> None: + """Connect to the AguaIOT platform""" + try: + await self.agua.connect() + except AguaIOTUpdateError as e: + _LOGGER.error("Agua IOT Update error: %s", e) + except AguaIOTUnauthorized as e: + raise UpdateFailed(f"Agua IOT Unauthorized: {e}") from e + except AguaIOTConnectionError as e: + raise UpdateFailed(f"Agua IOT Connection error: {e}") from e + except AguaIOTError as e: + raise UpdateFailed(f"Agua IOT error: {e}") from e + + async def _async_update_data(self) -> None: + """Get the latest data.""" + try: + await self.agua.update() + except AguaIOTUpdateError as e: + _LOGGER.error("Agua IOT Update error: %s", e) + except AguaIOTUnauthorized as e: + raise UpdateFailed(f"Agua IOT Unauthorized: {e}") from e + except AguaIOTConnectionError as e: + raise UpdateFailed(f"Agua IOT Connection error: {e}") from e + except AguaIOTError as e: + raise UpdateFailed(f"Agua IOT error: {e}") from e diff --git a/custom_components/aguaiot/diagnostics.py b/custom_components/aguaiot/diagnostics.py new file mode 100644 index 0000000..b5a2663 --- /dev/null +++ b/custom_components/aguaiot/diagnostics.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant + + +TO_REDACT = { + CONF_PASSWORD, + CONF_EMAIL, +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator = config_entry.runtime_data + agua = coordinator.agua + + devices = {} + for device in agua.devices: + devices[device.name] = {} + for reg in device.registers: + devices[device.name][reg] = device.get_register(reg) + + return { + "entry": async_redact_data(config_entry.as_dict(), TO_REDACT), + "devices": devices, + } diff --git a/custom_components/aguaiot/manifest.json b/custom_components/aguaiot/manifest.json new file mode 100644 index 0000000..4ceb6e8 --- /dev/null +++ b/custom_components/aguaiot/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "aguaiot", + "name": "Micronova Agua IOT", + "codeowners": ["@vincentwolsink"], + "config_flow": true, + "dependencies": [], + "documentation": "https://github.com/vincentwolsink/home_assistant_micronova_agua_iot/", + "integration_type": "hub", + "iot_class": "cloud_polling", + "issue_tracker": "https://github.com/vincentwolsink/home_assistant_micronova_agua_iot/issues", + "requirements": ["httpx", "simpleeval"], + "version": "0.9.2" +} diff --git a/custom_components/aguaiot/number.py b/custom_components/aguaiot/number.py new file mode 100644 index 0000000..9b54682 --- /dev/null +++ b/custom_components/aguaiot/number.py @@ -0,0 +1,85 @@ +import logging +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.number import NumberEntity +from homeassistant.helpers.entity import DeviceInfo +from .const import NUMBERS, DOMAIN +from .aguaiot import AguaIOTError + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + coordinator = config_entry.runtime_data + agua = coordinator.agua + + numbers = [] + for device in agua.devices: + hybrid = "power_wood_set" in device.registers + + for number in NUMBERS: + if ( + number.key in device.registers + and (number.force_enabled or device.get_register_enabled(number.key)) + and ( + (number.hybrid_only and hybrid) + or (number.hybrid_exclude and not hybrid) + or (not number.hybrid_only and not number.hybrid_exclude) + ) + ): + numbers.append(AguaIOTHeatingNumber(coordinator, device, number)) + + async_add_entities(numbers, True) + + +class AguaIOTHeatingNumber(CoordinatorEntity, NumberEntity): + """Number entity""" + + _attr_has_entity_name = True + + def __init__(self, coordinator, device, description): + """Initialize the thermostat.""" + super().__init__(coordinator) + self._device = device + self.entity_description = description + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._device.id_device}_{self.entity_description.key}" + + @property + def name(self): + """Return the name of the device, if any.""" + return self.entity_description.name + + @property + def device_info(self): + """Return the device info.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device.id_device)}, + name=self._device.name, + manufacturer="Micronova", + model=self._device.name_product, + ) + + @property + def native_value(self): + """Return the state of the sensor.""" + return self._device.get_register_value(self.entity_description.key) + + @property + def native_min_value(self): + return self._device.get_register_value_min(self.entity_description.key) + + @property + def native_max_value(self): + return self._device.get_register_value_max(self.entity_description.key) + + async def async_set_native_value(self, value): + try: + await self._device.set_register_value(self.entity_description.key, value) + await self.coordinator.async_request_refresh() + except (ValueError, AguaIOTError) as err: + _LOGGER.error("Failed to set value, error: %s", err)