From 64dbb8d4ad8152f5e2d3e522055f6b128dc0cc07 Mon Sep 17 00:00:00 2001 From: AndersonTomas Date: Thu, 8 Jan 2026 13:26:40 +0000 Subject: [PATCH] Upload files to "custom_components/aguaiot" --- custom_components/aguaiot/aguaiot.py | 711 +++++++++++++++++++++ custom_components/aguaiot/binary_sensor.py | 69 ++ custom_components/aguaiot/climate.py | 701 ++++++++++++++++++++ custom_components/aguaiot/config_flow.py | 228 +++++++ custom_components/aguaiot/const.py | 625 ++++++++++++++++++ 5 files changed, 2334 insertions(+) create mode 100644 custom_components/aguaiot/aguaiot.py create mode 100644 custom_components/aguaiot/binary_sensor.py create mode 100644 custom_components/aguaiot/climate.py create mode 100644 custom_components/aguaiot/config_flow.py create mode 100644 custom_components/aguaiot/const.py diff --git a/custom_components/aguaiot/aguaiot.py b/custom_components/aguaiot/aguaiot.py new file mode 100644 index 0000000..fc42278 --- /dev/null +++ b/custom_components/aguaiot/aguaiot.py @@ -0,0 +1,711 @@ +"""py_agua_iot provides controlling heating devices connected via +the IOT Agua platform of Micronova +""" + +import asyncio +import jwt +import logging +import time +import httpx +from simpleeval import simple_eval + +_LOGGER = logging.getLogger(__name__) + +API_PATH_APP_SIGNUP = "/appSignup" +API_PATH_LOGIN = "/userLogin" +API_PATH_REFRESH_TOKEN = "/refreshToken" +API_PATH_DEVICE_LIST = "/deviceList" +API_PATH_DEVICE_INFO = "/deviceGetInfo" +API_PATH_DEVICE_REGISTERS_MAP = "/deviceGetRegistersMap" +API_PATH_DEVICE_BUFFER_READING = "/deviceGetBufferReading" +API_PATH_DEVICE_JOB_STATUS = "/deviceJobStatus/" +API_PATH_DEVICE_WRITING = "/deviceRequestWriting" + +HEADER_ACCEPT = "application/json, text/javascript, */*; q=0.01" +HEADER_CONTENT_TYPE = "application/json" +HEADER = {"Accept": HEADER_ACCEPT, "Content-Type": HEADER_CONTENT_TYPE} + + +class aguaiot(object): + def __init__( + self, + api_url, + customer_code, + email, + password, + unique_id, + login_api_url=None, + brand_id=None, + brand=None, + application_version="1.9.7", + async_client=None, + air_temp_fix=False, + reading_error_fix=False, + language="ENG", + http_timeout=30, + buffer_read_timeout=30, + ): + self.api_url = api_url.rstrip("/") + self.customer_code = customer_code + self.email = email + self.password = password + self.unique_id = unique_id + self.brand_id = brand_id + self.brand = brand + self.login_api_url = login_api_url + self.application_version = application_version + self.token = None + self.token_expires = None + self.refresh_token = None + self.devices = list() + self.async_client = async_client + self.http_timeout = http_timeout + self.buffer_read_timeout = buffer_read_timeout + + # Vendor specific fixes + self.air_temp_fix = air_temp_fix + self.reading_error_fix = reading_error_fix + self.language = language + + if not self.async_client: + self.async_client = httpx.AsyncClient() + + async def connect(self): + await self.register_app_id() + await self.login() + await self.fetch_devices() + await self.fetch_device_information() + + def _headers(self): + """Correctly set headers for requests to Agua IOT.""" + + headers = { + "Accept": HEADER_ACCEPT, + "Content-Type": HEADER_CONTENT_TYPE, + "Origin": "file://", + "id_brand": self.brand_id if self.brand_id is not None else "1", + "customer_code": self.customer_code, + } + if self.brand is not None: + headers["brand"] = self.brand + + return headers + + async def register_app_id(self): + """Register app id with Agua IOT""" + + url = self.api_url + API_PATH_APP_SIGNUP + + payload = { + "phone_type": "Android", + "phone_id": self.unique_id, + "phone_version": "1.0", + "language": "en", + "id_app": self.unique_id, + "push_notification_token": self.unique_id, + "push_notification_active": False, + } + + try: + _LOGGER.debug( + "POST Register app - HEADERS: %s DATA: %s", self._headers(), payload + ) + async with self.async_client as client: + response = await client.post( + url, + json=payload, + headers=self._headers(), + follow_redirects=False, + timeout=self.http_timeout, + ) + _LOGGER.debug( + "RESPONSE Register app - CODE: %s DATA: %s", + response.status_code, + response.text, + ) + except httpx.TransportError as e: + raise AguaIOTConnectionError(f"Connection error to {url}: {e}") + + if response.status_code != 201: + _LOGGER.error( + "Failed to register app id. Code: %s, Response: %s", + response.status_code, + response.text, + ) + raise AguaIOTUnauthorized("Failed to register app id") + + return True + + async def login(self): + """Authenticate with email and password to Agua IOT""" + + url = self.api_url + API_PATH_LOGIN + + payload = {"email": self.email, "password": self.password} + extra_headers = {"local": "true", "Authorization": self.unique_id} + + headers = self._headers() + headers.update(extra_headers) + + if self.login_api_url is not None: + extra_login_headers = { + "applicationversion": self.application_version, + "url": API_PATH_LOGIN.lstrip("/"), + "userid": "null", + "aguaid": "null", + } + headers.update(extra_login_headers) + url = self.login_api_url + + try: + _LOGGER.debug("POST Login - HEADERS: %s DATA: ***", headers) + async with self.async_client as client: + response = await client.post( + url, + json=payload, + headers=headers, + follow_redirects=False, + timeout=self.http_timeout, + ) + _LOGGER.debug( + "RESPONSE Login - CODE: %s DATA: %s", + response.status_code, + response.text, + ) + except httpx.TransportError as e: + raise AguaIOTConnectionError(f"Connection error to {url}: {e}") + + if response.status_code != 200: + _LOGGER.error( + "Failed to login. Code: %s, Response: %s", + response.status_code, + response.text, + ) + raise AguaIOTUnauthorized("Failed to login, please check credentials") + + res = response.json() + self.token = res["token"] + self.refresh_token = res["refresh_token"] + + claimset = jwt.decode( + res["token"], options={"verify_signature": False}, algorithms=["none"] + ) + self.token_expires = claimset.get("exp") + + return True + + async def do_refresh_token(self): + """Refresh auth token for Agua IOT""" + + url = self.api_url + API_PATH_REFRESH_TOKEN + + payload = {"refresh_token": self.refresh_token} + + try: + _LOGGER.debug( + "POST Refresh token - HEADERS: %s DATA: %s", self._headers(), payload + ) + async with self.async_client as client: + response = await client.post( + url, + json=payload, + headers=self._headers(), + follow_redirects=False, + timeout=self.http_timeout, + ) + _LOGGER.debug( + "RESPONSE Refresh token - CODE: %s DATA: %s", + response.status_code, + response.text, + ) + except httpx.TransportError as e: + raise AguaIOTConnectionError(f"Connection error to {url}: {e}") + + if response.status_code != 201: + _LOGGER.warning("Refresh auth token failed, forcing new login...") + await self.login() + return + + res = response.json() + self.token = res["token"] + + claimset = jwt.decode( + res["token"], options={"verify_signature": False}, algorithms=["none"] + ) + self.token_expires = claimset.get("exp") + + return True + + async def fetch_devices(self): + """Fetch heating devices""" + url = self.api_url + API_PATH_DEVICE_LIST + + payload = {} + + res = await self.handle_webcall("POST", url, payload) + if res is False: + raise AguaIOTError("Error while fetching devices") + + for dev in res["device"]: + url = self.api_url + API_PATH_DEVICE_INFO + + payload = {"id_device": dev["id_device"], "id_product": dev["id_product"]} + res2 = await self.handle_webcall("POST", url, payload) + if res2 is False: + raise AguaIOTError("Error while fetching device info") + + self.devices.append( + Device( + dev["id"], + dev["id_device"], + dev["id_product"], + dev["product_serial"], + dev["name"], + dev["is_online"], + dev["name_product"], + res2["device_info"][0]["id_registers_map"], + self, + ) + ) + + async def fetch_device_information(self): + """Fetch device information of heating devices""" + for dev in self.devices: + await dev.update_mapping() + + async def update(self): + for dev in self.devices: + await dev.update() + + async def handle_webcall(self, method, url, payload): + if time.time() > self.token_expires: + await self.do_refresh_token() + + extra_headers = {"local": "false", "Authorization": self.token} + + headers = self._headers() + headers.update(extra_headers) + + try: + _LOGGER.debug("%s %s - HEADERS: %s DATA: %s", method, url, headers, payload) + if method == "POST": + async with self.async_client as client: + response = await client.post( + url, + json=payload, + headers=headers, + follow_redirects=False, + timeout=self.http_timeout, + ) + else: + async with self.async_client as client: + response = await client.get( + url, + params=payload, + headers=headers, + follow_redirects=False, + timeout=self.http_timeout, + ) + _LOGGER.debug( + "RESPONSE %s - CODE: %s DATA: %s", + url, + response.status_code, + response.text, + ) + except httpx.TransportError as e: + raise AguaIOTConnectionError(f"Connection error to {url}: {e}") + + if response.status_code == 401: + await self.do_refresh_token() + return await self.handle_webcall(method, url, payload) + elif response.status_code != 200: + _LOGGER.error( + "Webcall failed. Code: %s, Response: %s", + response.status_code, + response.text, + ) + return False + + return response.json() + + +class Device(object): + """Agua IOT heating device representation""" + + def __init__( + self, + id, + id_device, + id_product, + product_serial, + name, + is_online, + name_product, + id_registers_map, + aguaiot, + ): + self.id = id + self.id_device = id_device + self.id_product = id_product + self.product_serial = product_serial + self.name = name + self.is_online = is_online + self.name_product = name_product + self.id_registers_map = id_registers_map + self.__aguaiot = aguaiot + self.__register_map_dict = dict() + self.__information_dict = dict() + + async def update_mapping(self): + await self.__update_device_registers_mapping() + + async def update(self): + await self.__update_device_information() + + async def __update_device_registers_mapping(self): + url = self.__aguaiot.api_url + API_PATH_DEVICE_REGISTERS_MAP + registers = dict() + + payload = { + "id_device": self.id_device, + "id_product": self.id_product, + "last_update": "2018-06-03T08:59:54.043", + } + + res = await self.__aguaiot.handle_webcall("POST", url, payload) + if res is False: + raise AguaIOTError("Error while fetching registers map") + + for registers_map in res["device_registers_map"]["registers_map"]: + if registers_map["id"] == self.id_registers_map: + registers = { + reg["reg_key"].lower(): reg for reg in registers_map["registers"] + } + + self.__register_map_dict = registers + + async def __update_device_information(self): + url = self.__aguaiot.api_url + API_PATH_DEVICE_BUFFER_READING + + payload = { + "id_device": self.id_device, + "id_product": self.id_product, + "BufferId": 1, + } + + res_req = await self.__aguaiot.handle_webcall("POST", url, payload) + if res_req is False: + raise AguaIOTError("Error while making device buffer read request.") + + async def buffer_read_loop(id_request): + url = self.__aguaiot.api_url + API_PATH_DEVICE_JOB_STATUS + id_request + sleep_secs = 1 + attempts = 1 + + try: + while True: + await asyncio.sleep(sleep_secs) + + _LOGGER.debug("BUFFER READ (%s) ATTEMPT %s", id_request, attempts) + res_get = await self.__aguaiot.handle_webcall("GET", url, {}) + _LOGGER.debug( + "BUFFER READ (%s) STATUS: %s", + id_request, + res_get.get("jobAnswerStatus"), + ) + if res_get.get("jobAnswerStatus") != "waiting": + return res_get + + sleep_secs += 1 + attempts += 1 + except asyncio.CancelledError: + raise + + try: + res = await asyncio.wait_for( + buffer_read_loop(res_req["idRequest"]), + self.__aguaiot.buffer_read_timeout, + ) + except asyncio.TimeoutError: + raise AguaIOTUpdateError( + f"Timeout on waiting device buffer read to complete within {self.__aguaiot.buffer_read_timeout} seconds." + ) + + if not res: + raise AguaIOTUpdateError("Error while reading device buffer response.") + + if res.get("jobAnswerStatus") == "completed": + current_i = 0 + information_dict = dict() + try: + for item in res["jobAnswerData"]["Items"]: + information_dict.update( + {item: res["jobAnswerData"]["Values"][current_i]} + ) + current_i = current_i + 1 + except KeyError: + raise AguaIOTUpdateError("Error in data received from device.") + + self.__information_dict = information_dict + else: + raise AguaIOTUpdateError( + f"Received unexpected 'jobAnswerStatus' while reading buffers: {res.get('jobAnswerStatus')}" + ) + + def __prepare_value_for_writing(self, item, value, limit_value_raw=False): + set_min = self.__register_map_dict[item]["set_min"] + set_max = self.__register_map_dict[item]["set_max"] + + if not limit_value_raw and (float(value) < set_min or float(value) > set_max): + raise ValueError(f"Value must be between {set_min} and {set_max}: {value}") + + formula = self.__register_map_dict[item]["formula_inverse"] + formula = formula.replace("#", str(value)) + formula = formula.replace("Mod", "%") + eval_formula = simple_eval( + formula, + functions={"IF": lambda a, b, c: b if a else c, "int": lambda a: int(a)}, + ) + value = int(eval_formula) + + if limit_value_raw and (float(value) < set_min or float(value) > set_max): + raise ValueError( + f"Raw value must be between {set_min} and {set_max}: {value}" + ) + + if self.__register_map_dict[item]["is_hex"]: + value = int(f"0x{value}", 16) + + return value + + async def __request_writing(self, items): + url = self.__aguaiot.api_url + API_PATH_DEVICE_WRITING + + set_items = [] + set_masks = [] + set_bits = [] + set_endians = [] + set_values = [] + + for key in items: + set_items.append(int(self.__register_map_dict[key]["offset"])) + set_masks.append(int(self.__register_map_dict[key]["mask"])) + set_values.append(items[key]) + set_bits.append(8) + set_endians.append("L") + + payload = { + "id_device": self.id_device, + "id_product": self.id_product, + "Protocol": "RWMSmaster", + "BitData": set_bits, + "Endianess": set_endians, + "Items": set_items, + "Masks": set_masks, + "Values": set_values, + } + + res = await self.__aguaiot.handle_webcall("POST", url, payload) + if res is False: + raise AguaIOTError("Error while request device writing") + + id_request = res["idRequest"] + + url = self.__aguaiot.api_url + API_PATH_DEVICE_JOB_STATUS + id_request + + payload = {} + + retry_count = 0 + res = await self.__aguaiot.handle_webcall("GET", url, payload) + while ( + res is False or res["jobAnswerStatus"] != "completed" + ) and retry_count < 10: + await asyncio.sleep(1) + res = await self.__aguaiot.handle_webcall("GET", url, payload) + retry_count = retry_count + 1 + + if ( + res is False + or res["jobAnswerStatus"] != "completed" + or "Cmd" not in res["jobAnswerData"] + ): + raise AguaIOTError("Error while request device writing") + + @property + def registers(self): + return list(self.__register_map_dict.keys()) + + def get_register(self, key): + register = self.__register_map_dict.get(key, {}) + + try: + register["value_raw"] = str( + self.__information_dict[register["offset"]] & register["mask"] + ) + + formula = register["formula"].replace("#", register["value_raw"]) + formula = formula.replace("Mod", "%") + register["value"] = simple_eval( + formula, + functions={ + "IF": lambda a, b, c: b if a else c, + "int": lambda a: int(a), + }, + ) + except (KeyError, ValueError): + pass + + return register + + def get_register_value(self, key): + value = self.get_register(key).get("value") + + # Fix for reading errors from wifi module + if ( + self.__aguaiot.reading_error_fix + and int(self.get_register(key).get("value_raw", 0)) == 32768 + ): + _LOGGER.debug( + f"Applied reading_error_fix. Dropped value {value} for register {key}" + ) + return + + # Fix for stoves abusing air temp register + if ( + self.__aguaiot.air_temp_fix + and key.endswith("air_get") + and value + and int(value) > 100 + ): + _LOGGER.debug( + f"Applied air_temp_fix. Dropped value {value} for register {key}" + ) + return + + return value + + def get_register_value_min(self, key): + return self.get_register(key).get("set_min") + + def get_register_value_max(self, key): + return self.get_register(key).get("set_max") + + def get_register_value_formatted(self, key): + return str.format( + self.get_register(key).get("format_string"), + self.get_register(key).get("value"), + ) + + def get_register_value_description(self, key, language=None): + options = self.get_register_value_options(key, language) + if options: + return options.get( + self.get_register_value(key), self.get_register_value(key) + ) + else: + return self.get_register_value(key) + + def get_register_value_options(self, key, language=None): + if "enc_val" in self.get_register(key): + lang = language if language else self.__aguaiot.language + if lang not in self.get_register_value_options_languages(key): + lang = "ENG" + + return { + item["value"]: item["description"] + for item in self.get_register(key).get("enc_val") + if item["lang"] == lang + } + return {} + + def get_register_value_options_languages(self, key): + if "enc_val" in self.get_register(key): + return {item["lang"] for item in self.get_register(key).get("enc_val")} + return set() + + def get_register_enabled(self, key): + enable_key = key.rsplit("_", 1)[0] + "_enable" + if enable_key not in self.registers or not self.get_register(enable_key): + # Always enabled if no enable register present + return True + + if self.get_register(enable_key).get("reg_type") != "ENABLE": + raise AguaIOTError(f"Not a register of type ENABLE: {key}") + + if "enable_val" in self.get_register(enable_key): + enabled_values = [ + d["value"] for d in self.get_register(enable_key).get("enable_val") + ] + return self.get_register_value(enable_key) in enabled_values + else: + return self.get_register_value(enable_key) == 1 + + async def set_register_value(self, key, value, limit_value_raw=False): + if value is None: + raise AguaIOTError(f"Error while trying to set '{key}' to None") + + value = self.__prepare_value_for_writing( + key, value, limit_value_raw=limit_value_raw + ) + items = {key: value} + + try: + await self.__request_writing(items) + except AguaIOTError: + raise AguaIOTError(f"Error while trying to set: key={key} value={value}") + + async def set_register_values(self, items, limit_value_raw=False): + for key in items: + items[key] = self.__prepare_value_for_writing( + key, items[key], limit_value_raw=limit_value_raw + ) + + try: + await self.__request_writing(items) + except AguaIOTError: + raise AguaIOTError(f"Error while trying to set: items={items}") + + async def set_register_value_description( + self, key, value_description, value_fallback=None, language=None + ): + try: + options = self.get_register_value_options(key, language) + value = list(options.keys())[ + list(options.values()).index(value_description) + ] + except (AttributeError, ValueError): + value = value_description + try: + value = float(value) + except ValueError: + value = value_fallback + + await self.set_register_value(key, value) + + +class AguaIOTError(Exception): + """Exception type for Agua IOT""" + + def __init__(self, message): + Exception.__init__(self, message) + + +class AguaIOTUnauthorized(AguaIOTError): + """Unauthorized""" + + def __init__(self, message): + super().__init__(message) + + +class AguaIOTConnectionError(AguaIOTError): + """Connection error""" + + def __init__(self, message): + super().__init__(message) + + +class AguaIOTUpdateError(AguaIOTError): + """Update error""" + + def __init__(self, message): + super().__init__(message) diff --git a/custom_components/aguaiot/binary_sensor.py b/custom_components/aguaiot/binary_sensor.py new file mode 100644 index 0000000..2ea02dd --- /dev/null +++ b/custom_components/aguaiot/binary_sensor.py @@ -0,0 +1,69 @@ +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers.entity import DeviceInfo +from .const import BINARY_SENSORS, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + coordinator = config_entry.runtime_data + agua = coordinator.agua + + sensors = [] + for device in agua.devices: + hybrid = "power_wood_set" in device.registers + + for sensor in BINARY_SENSORS: + if ( + sensor.key in device.registers + and (sensor.force_enabled or device.get_register_enabled(sensor.key)) + and (not sensor.hybrid_only or hybrid) + ): + sensors.append(AguaIOTHeatingBinarySensor(coordinator, device, sensor)) + + async_add_entities(sensors, True) + + +class AguaIOTHeatingBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Binary sensor 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 icon(self): + if self.is_on: + return self.entity_description.icon_on or self.entity_description.icon + else: + return self.entity_description.icon + + @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 is_on(self): + """Return the state of the sensor.""" + return bool(self._device.get_register_value(self.entity_description.key)) diff --git a/custom_components/aguaiot/climate.py b/custom_components/aguaiot/climate.py new file mode 100644 index 0000000..0fea2c1 --- /dev/null +++ b/custom_components/aguaiot/climate.py @@ -0,0 +1,701 @@ +"""Support for Agua IOT heating devices.""" + +import logging +import re +import copy +import numbers +from homeassistant.helpers import entity_platform +from homeassistant.util import dt +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + HVACAction, + HVACMode, + ClimateEntityFeature, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + UnitOfTemperature, + PRECISION_HALVES, +) +from .const import ( + DOMAIN, + AIR_VARIANTS, + WATER_VARIANTS, + CLIMATE_CANALIZATIONS, + MODE_PELLETS, + MODE_WOOD, + STATUS_OFF, + STATUS_IDLE, +) +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 + + entities = [] + for device in agua.devices: + stove = AguaIOTAirDevice(coordinator, device) + entities.append(stove) + + if any(f"temp_{variant}_set" in device.registers for variant in WATER_VARIANTS): + entities.append(AguaIOTWaterDevice(coordinator, device, stove)) + + for canalization in CLIMATE_CANALIZATIONS: + for c_found in [ + m + for i in device.registers + for m in [re.match(canalization.key, i.lower())] + if m + ]: + if ( + ( + canalization.key_enable + and device.get_register_enabled( + canalization.key_enable.format(id=c_found.group(1)) + ) + ) + or ( + canalization.key2_enable + and device.get_register_enabled( + canalization.key2_enable.format(id=c_found.group(1)) + ) + ) + or ( + not canalization.key_enable + and device.get_register_enabled(c_found.group(0)) + ) + ): + c_copy = copy.deepcopy(canalization) + c_copy.key = c_found.group(0) + for key in [ + "name", + "key_temp_set", + "key_temp_get", + "key_temp2_get", + "key_vent_set", + ]: + if getattr(c_copy, key): + setattr( + c_copy, + key, + getattr(c_copy, key).format_map(c_found.groupdict()), + ) + + entities.append( + AguaIOTCanalizationDevice(coordinator, device, c_copy, stove) + ) + + async_add_entities(entities, True) + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + "sync_clock", + {}, + "sync_clock", + ) + + +class AguaIOTClimateDevice(CoordinatorEntity, ClimateEntity): + @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 temperature_unit(self): + """Return the unit of measurement.""" + return UnitOfTemperature.CELSIUS + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_HALVES + + +class AguaIOTAirDevice(AguaIOTClimateDevice): + """Representation of an Agua IOT heating device.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, coordinator, device): + """Initialize the thermostat.""" + super().__init__(coordinator) + self._enable_turn_on_off_backwards_compatibility = False + self._device = device + self._hybrid = "power_wood_set" in device.registers + + self._temperature_get_key = None + for variant in AIR_VARIANTS: + if ( + f"temp_{variant}_get" in self._device.registers + and self._device.get_register_enabled(f"temp_{variant}_get") + and self._device.get_register_value(f"temp_{variant}_get") + ): + self._temperature_get_key = f"temp_{variant}_get" + break + + self._temperature_set_key = None + for variant in AIR_VARIANTS: + if ( + f"temp_{variant}_set" in self._device.registers + and self._device.get_register_enabled(f"temp_{variant}_set") + and self._device.get_register_value(f"temp_{variant}_set") + ): + self._temperature_set_key = f"temp_{variant}_set" + break + + @property + def unique_id(self): + """Return a unique ID.""" + return self._device.id_device + + @property + def supported_features(self): + """Return the list of supported features.""" + features = ( + ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + ) + if self._hybrid: + features = features | ClimateEntityFeature.PRESET_MODE + return features + + @property + def hvac_action(self): + """Return the current running hvac operation.""" + if self._device.get_register_value("status_get") is not None: + if ( + str( + self._device.get_register_value_description( + key="status_get", language="ENG" + ) + ).upper() + in STATUS_IDLE + ): + return HVACAction.IDLE + elif ( + self._device.get_register_value("status_get") == 0 + or str( + self._device.get_register_value_description( + key="status_get", language="ENG" + ) + ).upper() + in STATUS_OFF + ): + return HVACAction.OFF + return HVACAction.HEATING + + @property + def hvac_modes(self): + """Return the list of available hvac operation modes.""" + return [HVACMode.HEAT, HVACMode.OFF] + + @property + def hvac_mode(self): + """Return hvac operation ie. heat, cool mode.""" + if self._device.get_register_value("status_get") is not None: + if ( + self._device.get_register_value("status_get") == 0 + or str( + self._device.get_register_value_description( + key="status_get", language="ENG" + ) + ).upper() + in STATUS_OFF + ): + return HVACMode.OFF + return HVACMode.HEAT + + async def async_set_hvac_mode(self, hvac_mode): + """Set new target hvac mode.""" + if hvac_mode == HVACMode.OFF: + await self.async_turn_off() + elif hvac_mode == HVACMode.HEAT: + await self.async_turn_on() + + @property + def hybrid_mode(self): + return ( + MODE_WOOD + if self._hybrid and self._device.get_register_enabled("real_power_wood_get") + else MODE_PELLETS + ) + + @property + def fan_mode(self): + """Return fan mode.""" + power_register = ( + "power_wood_set" if self.hybrid_mode == MODE_WOOD else "power_set" + ) + return str(self._device.get_register_value_description(power_register)) + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + fan_modes = [] + power_register = ( + "power_wood_set" if self.hybrid_mode == MODE_WOOD else "power_set" + ) + for x in range( + self._device.get_register_value_min(power_register), + (self._device.get_register_value_max(power_register) + 1), + ): + fan_modes.append( + str(self._device.get_register_value_options(power_register).get(x, x)) + ) + return fan_modes + + @property + def preset_modes(self): + return [self.hybrid_mode] + + @property + def preset_mode(self): + return self.hybrid_mode + + async def async_set_preset_mode(self, preset_mode): + # The stove will pick the correct mode. + pass + + async def async_turn_off(self): + """Turn device off.""" + try: + await self._device.set_register_value_description( + key="status_managed_get", + value_description="OFF", + value_fallback=170, + language="ENG", + ) + await self.coordinator.async_request_refresh() + except AguaIOTError as err: + _LOGGER.error("Failed to turn off device, error: %s", err) + + async def async_turn_on(self): + """Turn device on.""" + try: + await self._device.set_register_value_description( + key="status_managed_get", + value_description="ON", + value_fallback=85, + language="ENG", + ) + await self.coordinator.async_request_refresh() + except AguaIOTError as err: + _LOGGER.error("Failed to turn on device, error: %s", err) + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + power_register = ( + "power_wood_set" if self.hybrid_mode == MODE_WOOD else "power_set" + ) + try: + await self._device.set_register_value_description(power_register, fan_mode) + await self.coordinator.async_request_refresh() + except AguaIOTError as err: + _LOGGER.error("Failed to set fan mode, error: %s", err) + + @property + def min_temp(self): + """Return the minimum temperature to set.""" + return self._device.get_register_value_min(self._temperature_set_key) + + @property + def max_temp(self): + """Return the maximum temperature to set.""" + return self._device.get_register_value_max(self._temperature_set_key) + + @property + def current_temperature(self): + """Return the current temperature.""" + if self._temperature_get_key: + value = self._device.get_register_value_description( + self._temperature_get_key + ) + if isinstance(value, numbers.Number): + return value + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.current_temperature: + return self._device.get_register_value(self._temperature_set_key) + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + + try: + await self._device.set_register_value( + self._temperature_set_key, temperature + ) + await self.coordinator.async_request_refresh() + except (ValueError, AguaIOTError) as err: + _LOGGER.error("Failed to set temperature, error: %s", err) + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._device.get_register(self._temperature_set_key).get("step", 1) + + async def sync_clock(self): + dt_now = dt.now() + try: + await self._device.set_register_values( + { + "clock_hour_set": dt_now.hour, + "clock_minute_set": dt_now.minute, + "calendar_day_set": dt_now.day, + "calendar_month_set": dt_now.month, + "calendar_year_set": dt_now.year, + }, + limit_value_raw=True, + ) + except (ValueError, AguaIOTError) as err: + _LOGGER.error("Failed to set value, error: %s", err) + + +class AguaIOTWaterDevice(AguaIOTClimateDevice): + """Representation of an Agua IOT heating device.""" + + _attr_has_entity_name = True + _attr_name = "Water" + _attr_icon = "mdi:water" + + def __init__(self, coordinator, device, parent): + """Initialize the thermostat.""" + super().__init__(coordinator) + self._enable_turn_on_off_backwards_compatibility = False + self._device = device + self._parent = parent + + self._temperature_get_key = None + for variant in WATER_VARIANTS: + if ( + f"temp_{variant}_get" in self._device.registers + and self._device.get_register_enabled(f"temp_{variant}_get") + and self._device.get_register_value(f"temp_{variant}_get") + ): + self._temperature_get_key = f"temp_{variant}_get" + break + + self._temperature_set_key = None + for variant in WATER_VARIANTS: + if ( + f"temp_{variant}_set" in self._device.registers + and self._device.get_register_enabled(f"temp_{variant}_set") + and self._device.get_register_value(f"temp_{variant}_set") + ): + self._temperature_set_key = f"temp_{variant}_set" + break + + @property + def unique_id(self): + return f"{self._device.id_device}_water" + + @property + def supported_features(self): + """Return the list of supported features.""" + features = ( + ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TARGET_TEMPERATURE + ) + return features + + @property + def hvac_action(self): + return self._parent.hvac_action + + @property + def hvac_modes(self): + return self._parent.hvac_modes + + @property + def hvac_mode(self): + return self._parent.hvac_mode + + async def async_set_hvac_mode(self, hvac_mode): + """Set new target hvac mode.""" + if hvac_mode == HVACMode.OFF: + await self.async_turn_off() + elif hvac_mode == HVACMode.HEAT: + await self.async_turn_on() + + async def async_turn_off(self): + """Turn device off.""" + try: + await self._device.set_register_value_description( + key="status_managed_get", + value_description="OFF", + value_fallback=170, + language="ENG", + ) + await self.coordinator.async_request_refresh() + except AguaIOTError as err: + _LOGGER.error("Failed to turn off device, error: %s", err) + + async def async_turn_on(self): + """Turn device on.""" + try: + await self._device.set_register_value_description( + key="status_managed_get", + value_description="ON", + value_fallback=85, + language="ENG", + ) + await self.coordinator.async_request_refresh() + except AguaIOTError as err: + _LOGGER.error("Failed to turn on device, error: %s", err) + + @property + def min_temp(self): + """Return the minimum temperature to set.""" + return self._device.get_register_value_min(self._temperature_set_key) + + @property + def max_temp(self): + """Return the maximum temperature to set.""" + return self._device.get_register_value_max(self._temperature_set_key) + + @property + def current_temperature(self): + """Return the current temperature.""" + if self._temperature_get_key: + value = self._device.get_register_value_description( + self._temperature_get_key + ) + if isinstance(value, numbers.Number): + return value + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.current_temperature: + return self._device.get_register_value(self._temperature_set_key) + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + + try: + await self._device.set_register_value( + self._temperature_set_key, temperature + ) + await self.coordinator.async_request_refresh() + except (ValueError, AguaIOTError) as err: + _LOGGER.error("Failed to set temperature, error: %s", err) + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._device.get_register(self._temperature_set_key).get("step", 1) + + +class AguaIOTCanalizationDevice(AguaIOTClimateDevice): + """Canalization device""" + + _attr_has_entity_name = True + + def __init__(self, coordinator, device, description, parent): + super().__init__(coordinator) + self._enable_turn_on_off_backwards_compatibility = False + self._device = device + self._parent = parent + self.entity_description = description + self._fan_register = self.entity_description.key + + if ( + self.entity_description.key_vent_set + and self.entity_description.key_vent_set in self._device.registers + ): + self._fan_register = self.entity_description.key_vent_set + + @property + def unique_id(self): + return f"{self._device.id_device}_{self.entity_description.key}" + + @property + def name(self): + return self.entity_description.name + + @property + def supported_features(self): + features = ClimateEntityFeature.FAN_MODE + if ( + self.entity_description.key_temp_set + and self.entity_description.key_temp_set in self._device.registers + and self._device.get_register_enabled(self.entity_description.key_temp_set) + ): + features |= ClimateEntityFeature.TARGET_TEMPERATURE + if ( + self.entity_description.key_vent_set + and self.entity_description.key_vent_set in self._device.registers + ): + features |= ClimateEntityFeature.PRESET_MODE + + return features + + @property + def fan_mode(self): + """Return fan mode.""" + return str(self._device.get_register_value_description(self._fan_register)) + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + fan_modes = [] + for x in range( + self._device.get_register_value_min(self._fan_register), + (self._device.get_register_value_max(self._fan_register) + 1), + ): + fan_modes.append( + str( + self._device.get_register_value_options(self._fan_register).get( + x, x + ) + ) + ) + return fan_modes + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + try: + await self._device.set_register_value_description( + self._fan_register, fan_mode + ) + await self.coordinator.async_request_refresh() + except AguaIOTError as err: + _LOGGER.error("Failed to set fan mode, error: %s", err) + + @property + def preset_modes(self): + return list( + self._device.get_register_value_options( + self.entity_description.key + ).values() + ) + + @property + def preset_mode(self): + return self._device.get_register_value_description(self.entity_description.key) + + async def async_set_preset_mode(self, preset_mode): + """Set new target preset mode.""" + try: + await self._device.set_register_value_description( + self.entity_description.key, preset_mode + ) + await self.coordinator.async_request_refresh() + except AguaIOTError as err: + _LOGGER.error("Failed to set preset mode, error: %s", err) + + @property + def hvac_action(self): + if self._device.get_register_value(self.entity_description.key): + return self._parent.hvac_action + return HVACAction.OFF + + @property + def hvac_modes(self): + if self._device.get_register_value(self.entity_description.key): + return [self._parent.hvac_mode] + return [HVACMode.OFF] + + @property + def hvac_mode(self): + if self._device.get_register_value(self.entity_description.key): + return self._parent.hvac_mode + return HVACMode.OFF + + async def async_set_hvac_mode(self, hvac_mode): + pass + + @property + def min_temp(self): + """Return the minimum temperature to set.""" + if self.entity_description.key_temp_set in self._device.registers: + return self._device.get_register_value_min( + self.entity_description.key_temp_set + ) + + @property + def max_temp(self): + """Return the maximum temperature to set.""" + if self.entity_description.key_temp_set in self._device.registers: + return self._device.get_register_value_max( + self.entity_description.key_temp_set + ) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.entity_description.key_temp_set in self._device.registers: + if self.current_temperature: + return self._device.get_register_value( + self.entity_description.key_temp_set + ) + + @property + def current_temperature(self): + """Return the current temperature.""" + if ( + self.entity_description.key_temp_get in self._device.registers + and self._device.get_register_enabled(self.entity_description.key_temp_get) + ): + value = self._device.get_register_value_description( + self.entity_description.key_temp_get + ) + if isinstance(value, numbers.Number): + return value + elif ( + self.entity_description.key_temp2_get + and self.entity_description.key_temp2_get in self._device.registers + and self._device.get_register_enabled(self.entity_description.key_temp2_get) + ): + value = self._device.get_register_value_description( + self.entity_description.key_temp2_get + ) + if isinstance(value, numbers.Number): + return value + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + + try: + await self._device.set_register_value( + self.entity_description.key_temp_set, temperature + ) + await self.coordinator.async_request_refresh() + except (ValueError, AguaIOTError) as err: + _LOGGER.error("Failed to set temperature, error: %s", err) + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + if self.entity_description.key_temp_set in self._device.registers: + return self._device.get_register(self.entity_description.key_temp_set).get( + "step", 1 + ) diff --git a/custom_components/aguaiot/config_flow.py b/custom_components/aguaiot/config_flow.py new file mode 100644 index 0000000..d70d352 --- /dev/null +++ b/custom_components/aguaiot/config_flow.py @@ -0,0 +1,228 @@ +"""Config flow for Agua IOT.""" + +import logging +import uuid + +from .aguaiot import ( + AguaIOTConnectionError, + AguaIOTError, + AguaIOTUnauthorized, + aguaiot, +) +import voluptuous as vol + +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + OptionsFlowWithReload, + CONN_CLASS_CLOUD_POLL, +) +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import callback +from homeassistant.helpers.httpx_client import get_async_client + +from .const import ( + CONF_API_URL, + CONF_CUSTOMER_CODE, + CONF_LOGIN_API_URL, + CONF_UUID, + CONF_ENDPOINT, + 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, + ENDPOINTS, +) + +_LOGGER = logging.getLogger(__name__) + + +def conf_entries(hass): + """Return the email tuples for the domain.""" + return set( + (entry.data[CONF_EMAIL], entry.data[CONF_API_URL]) + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class AguaIOTConfigFlow(ConfigFlow, domain=DOMAIN): + """Agua IOT Config Flow handler.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL + + def _entry_in_configuration_exists(self, user_input) -> bool: + """Return True if config already exists in configuration.""" + email = user_input[CONF_EMAIL] + host_server = ENDPOINTS[user_input[CONF_ENDPOINT]][CONF_API_URL] + if (email, host_server) in conf_entries(self.hass): + return True + return False + + async def async_step_user(self, user_input=None): + """User initiated integration.""" + errors = {} + if user_input is not None: + # Validate user input + email = user_input[CONF_EMAIL] + password = user_input[CONF_PASSWORD] + + endpoint = user_input[CONF_ENDPOINT] + api_url = ENDPOINTS[endpoint][CONF_API_URL] + customer_code = ENDPOINTS[endpoint][CONF_CUSTOMER_CODE] + login_api_url = ENDPOINTS[endpoint].get(CONF_LOGIN_API_URL) + brand_id = ENDPOINTS[endpoint].get(CONF_BRAND_ID) + brand = ENDPOINTS[endpoint].get(CONF_BRAND) + + if self._entry_in_configuration_exists(user_input): + return self.async_abort(reason="device_already_configured") + + try: + gen_uuid = str(uuid.uuid1()) + 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(self.hass), + ) + await agua.connect() + except AguaIOTUnauthorized as e: + _LOGGER.error("Agua IOT Unauthorized: %s", e) + errors["base"] = "unauthorized" + except AguaIOTConnectionError as e: + _LOGGER.error("Agua IOT Connection error: %s", e) + errors["base"] = "connection_error" + except AguaIOTError as e: + _LOGGER.error("Agua IOT error: %s", e) + errors["base"] = "unknown_error" + + if "base" not in errors: + return self.async_create_entry( + title=endpoint, + data={ + CONF_EMAIL: email, + CONF_PASSWORD: password, + CONF_UUID: gen_uuid, + CONF_API_URL: api_url, + CONF_CUSTOMER_CODE: customer_code, + CONF_LOGIN_API_URL: login_api_url, + CONF_BRAND_ID: brand_id, + CONF_BRAND: brand, + }, + ) + else: + user_input = {} + + data_schema = vol.Schema( + { + vol.Required( + CONF_ENDPOINT, default=user_input.get(CONF_ENDPOINT) + ): vol.In(ENDPOINTS.keys()), + vol.Required(CONF_EMAIL, default=user_input.get(CONF_EMAIL)): str, + vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD)): str, + } + ) + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry): + return AguaIOTOptionsFlowHandler() + + +class AguaIOTOptionsFlowHandler(OptionsFlowWithReload): + async def async_step_init(self, _user_input=None): + """Manage the options.""" + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + """Set up AguaIOT entry.""" + entry = self.config_entry + + api_url = entry.data.get(CONF_API_URL) + customer_code = entry.data.get(CONF_CUSTOMER_CODE) + email = entry.data.get(CONF_EMAIL) + password = entry.data.get(CONF_PASSWORD) + gen_uuid = entry.data.get(CONF_UUID) + login_api_url = entry.data.get(CONF_LOGIN_API_URL) + brand_id = entry.data.get(CONF_BRAND_ID) + brand = entry.data.get(CONF_BRAND) + + 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(self.hass), + ) + + try: + await agua.connect() + except AguaIOTUnauthorized as e: + _LOGGER.error("Agua IOT Unauthorized: %s", e) + return False + except AguaIOTConnectionError as e: + _LOGGER.error("Agua IOT Connection error: %s", e) + return False + except AguaIOTError as e: + _LOGGER.error("Agua IOT error: %s", e) + return False + + languages = ["ENG"] + if agua.devices: + languages = sorted( + list( + agua.devices[0].get_register_value_options_languages( + "status_managed_get" + ) + ) + ) + + schema = { + vol.Optional( + CONF_AIR_TEMP_FIX, + default=self.config_entry.options.get(CONF_AIR_TEMP_FIX, False), + ): bool, + vol.Optional( + CONF_READING_ERROR_FIX, + default=self.config_entry.options.get(CONF_READING_ERROR_FIX, False), + ): bool, + vol.Optional( + CONF_UPDATE_INTERVAL, + default=self.config_entry.options.get(CONF_UPDATE_INTERVAL, 60), + ): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional( + CONF_HTTP_TIMEOUT, + default=self.config_entry.options.get(CONF_HTTP_TIMEOUT, 30), + ): vol.All(vol.Coerce(int), vol.Range(max=60)), + vol.Optional( + CONF_BUFFER_READ_TIMEOUT, + default=self.config_entry.options.get(CONF_BUFFER_READ_TIMEOUT, 30), + ): vol.All(vol.Coerce(int), vol.Range(max=60)), + vol.Optional( + CONF_LANGUAGE, + default=self.config_entry.options.get(CONF_LANGUAGE, "ENG"), + ): vol.In(languages), + } + return self.async_show_form(step_id="user", data_schema=vol.Schema(schema)) diff --git a/custom_components/aguaiot/const.py b/custom_components/aguaiot/const.py new file mode 100644 index 0000000..e2e71be --- /dev/null +++ b/custom_components/aguaiot/const.py @@ -0,0 +1,625 @@ +"""Agua IOT constants.""" + +from homeassistant.const import ( + Platform, + UnitOfTemperature, + UnitOfPressure, + REVOLUTIONS_PER_MINUTE, +) +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntityDescription, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntityDescription, +) +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntityDescription, +) +from homeassistant.components.climate import ( + ClimateEntityDescription, +) +from homeassistant.components.select import SelectEntityDescription +from dataclasses import dataclass + + +@dataclass +class AguaIOTBinarySensorEntityDescription(BinarySensorEntityDescription): + force_enabled: bool = False + hybrid_only: bool = False + icon_on: str | None = None + + +@dataclass +class AguaIOTSensorEntityDescription(SensorEntityDescription): + force_enabled: bool = False + hybrid_only: bool = False + hybrid_exclude: bool = False + raw_value: bool = False + + +@dataclass +class AguaIOTNumberEntityDescription(NumberEntityDescription): + force_enabled: bool = False + hybrid_only: bool = False + hybrid_exclude: bool = False + + +@dataclass +class AguaIOTCanalizationEntityDescription(ClimateEntityDescription): + key_temp_set: str | None = None + key_temp_get: str | None = None + key_temp2_get: str | None = None + key_vent_set: str | None = None + key_enable: str | None = None + key2_enable: str | None = None + + +DOMAIN = "aguaiot" +CONF_API_URL = "api_url" +CONF_CUSTOMER_CODE = "customer_code" +CONF_LOGIN_API_URL = "login_api_url" +CONF_UUID = "uuid" +CONF_ENDPOINT = "endpoint" +CONF_BRAND_ID = "brand_id" +CONF_BRAND = "brand" +CONF_LANGUAGE = "language" +CONF_AIR_TEMP_FIX = "air_temp_fix" +CONF_READING_ERROR_FIX = "reading_error_fix" +CONF_UPDATE_INTERVAL = "update_interval" +CONF_HTTP_TIMEOUT = "http_timeout" +CONF_BUFFER_READ_TIMEOUT = "buffer_read_timeout" + +AIR_VARIANTS = ["air", "air2", "air3", "air_palm"] +WATER_VARIANTS = ["water", "h2o", "h2o_mandata"] + +MODE_WOOD = "Wood" +MODE_PELLETS = "Pellet" + +STATUS_OFF = ["OFF", "FINAL CLEANING", "STOP", "SHUT OFF", "0", "6"] +STATUS_IDLE = [ + "ECO STOP", + "STANDBY", + "STAND BY", + "STAND-BY", + "ALARM", + "MEMORY ALARM", + "ALARM MEMORY", + "MEM.ALM", + "MEM. ALARM", + "7", + "8", + "9", +] + +PLATFORMS = [ + Platform.CLIMATE, + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.SWITCH, + Platform.NUMBER, + Platform.SELECT, +] + +ENDPOINTS = { + "Alfapalm": { + CONF_CUSTOMER_CODE: "862148", + CONF_API_URL: "https://alfaplam.agua-iot.com", + }, + "APP-O BIOEN": { + CONF_CUSTOMER_CODE: "289982", + CONF_API_URL: "https://unical.agua-iot.com", + }, + "Boreal Home": { + CONF_CUSTOMER_CODE: "173118", + CONF_API_URL: "https://boreal.agua-iot.com", + }, + "Bronpi Home": { + CONF_CUSTOMER_CODE: "164873", + CONF_API_URL: "https://bronpi.agua-iot.com", + }, + "Darwin Evolution": { + CONF_CUSTOMER_CODE: "475219", + CONF_API_URL: "https://cola.agua-iot.com", + }, + "Easy Connect": { + CONF_CUSTOMER_CODE: "354924", + CONF_API_URL: "https://remote.mcz.it", + }, + "Easy Connect Plus": { + CONF_CUSTOMER_CODE: "746318", + CONF_API_URL: "https://remote.mcz.it", + }, + "Easy Connect Poêle": { + CONF_CUSTOMER_CODE: "354925", + CONF_API_URL: "https://remote.mcz.it", + }, + "Elcofire Pellet Home": { + CONF_CUSTOMER_CODE: "132679", + CONF_API_URL: "https://elcofire.agua-iot.com", + }, + "Elfire Wifi": { + CONF_CUSTOMER_CODE: "402762", + CONF_API_URL: "https://elfire.agua-iot.com", + }, + "EvaCalòr - PuntoFuoco": { + CONF_CUSTOMER_CODE: "635987", + CONF_API_URL: "https://evastampaggi.agua-iot.com", + }, + "Fontana Forni": { + CONF_CUSTOMER_CODE: "505912", + CONF_API_URL: "https://fontanaforni.agua-iot.com", + }, + "Fonte Flamme contrôle 1": { + CONF_CUSTOMER_CODE: "848324", + CONF_API_URL: "https://fonteflame.agua-iot.com", + }, + "Globe-fire": { + CONF_CUSTOMER_CODE: "634876", + CONF_API_URL: "https://globefire.agua-iot.com", + }, + "GO HEAT": { + CONF_CUSTOMER_CODE: "859435", + CONF_API_URL: "https://amg.agua-iot.com", + }, + "Jolly Mec Wi Fi": { + CONF_CUSTOMER_CODE: "732584", + CONF_API_URL: "https://jollymec.agua-iot.com", + }, + "Karmek Wifi": { + CONF_CUSTOMER_CODE: "403873", + CONF_API_URL: "https://karmekone.agua-iot.com", + }, + "Klover Home": { + CONF_CUSTOMER_CODE: "143789", + CONF_API_URL: "https://klover.agua-iot.com", + }, + "L'artistico": { + CONF_CUSTOMER_CODE: "635912", + CONF_API_URL: "https://api.micronovasrl.com", + }, + "LAMINOX Remote Control (2.0)": { + CONF_CUSTOMER_CODE: "352678", + CONF_API_URL: "https://laminox.agua-iot.com", + }, + "Lorflam Home": { + CONF_CUSTOMER_CODE: "121567", + CONF_API_URL: "https://lorflam.agua-iot.com", + }, + "Moretti design": { + CONF_CUSTOMER_CODE: "624813", + CONF_API_URL: "https://moretti.agua-iot.com", + }, + "My Corisit": { + CONF_CUSTOMER_CODE: "101427", + CONF_API_URL: "https://mycorisit.agua-iot.com", + }, + "MyPiazzetta": { + CONF_CUSTOMER_CODE: "458632", + CONF_API_URL: "https://piazzetta.agua-iot.com", + CONF_LOGIN_API_URL: "https://piazzetta-iot.app2cloud.it/api/bridge/endpoint/", + }, + "MySuperior": { + CONF_CUSTOMER_CODE: "458632", + CONF_API_URL: "https://piazzetta.agua-iot.com", + CONF_LOGIN_API_URL: "https://piazzetta-iot.app2cloud.it/api/bridge/endpoint/", + CONF_BRAND_ID: "2", + CONF_BRAND: "superior", + }, + "Nina": { + CONF_CUSTOMER_CODE: "999999", + CONF_API_URL: "https://micronova.agua-iot.com", + }, + "Nobis-Fi": { + CONF_CUSTOMER_CODE: "700700", + CONF_API_URL: "https://nobis.agua-iot.com", + }, + "Nordic Fire 2.0": { + CONF_CUSTOMER_CODE: "132678", + CONF_API_URL: "https://nordicfire.agua-iot.com", + }, + "Ravelli Wi-Fi": { + CONF_CUSTOMER_CODE: "953712", + CONF_API_URL: "https://ravelli.agua-iot.com", + }, + "Stufe a pellet Italia": { + CONF_CUSTOMER_CODE: "015142", + CONF_API_URL: "https://stufepelletitalia.agua-iot.com", + }, + "Thermoflux": { + CONF_CUSTOMER_CODE: "391278", + CONF_API_URL: "https://thermoflux.agua-iot.com", + }, + "Total Control 3.0 (Extraflame)": { + CONF_CUSTOMER_CODE: "195764", + CONF_API_URL: "https://extraflame.agua-iot.com/", + }, + "TS Smart": { + CONF_CUSTOMER_CODE: "046629", + CONF_API_URL: "https://timsistem.agua-iot.com", + }, + "TurboFonte": { + CONF_CUSTOMER_CODE: "354924", + CONF_API_URL: "https://remote.mcz.it", + CONF_BRAND_ID: "2", + CONF_BRAND: "turbofonte", + }, + "Wi-Phire": { + CONF_CUSTOMER_CODE: "521228", + CONF_API_URL: "https://lineavz.agua-iot.com", + }, +} + +BINARY_SENSORS = ( + AguaIOTBinarySensorEntityDescription( + key="ris_pellet_ris_get", + name="Pellets Depleted", + icon="mdi:fire", + icon_on="mdi:fire-alert", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + AguaIOTBinarySensorEntityDescription( + key="popup_riserva_wood_get", + name="Wood Reserve", + icon="mdi:fire", + icon_on="mdi:fire-alert", + device_class=BinarySensorDeviceClass.PROBLEM, + force_enabled=True, + ), + AguaIOTBinarySensorEntityDescription( + key="thermostat_contact_get", + name="External Thermostat", + icon="mdi:electric-switch", + icon_on="mdi:electric-switch-closed", + ), + AguaIOTBinarySensorEntityDescription( + key="thermostat_contact_rear_get", + name="External Thermostat Rear", + icon="mdi:electric-switch", + icon_on="mdi:electric-switch-closed", + ), +) + +SENSORS = ( + AguaIOTSensorEntityDescription( + key="status_get", + name="Status", + icon="mdi:fire", + native_unit_of_measurement=None, + state_class=None, + device_class=SensorDeviceClass.ENUM, + ), + AguaIOTSensorEntityDescription( + key="alarms_get", + name="Alarm", + icon="mdi:alert-outline", + native_unit_of_measurement=None, + state_class=None, + device_class=SensorDeviceClass.ENUM, + force_enabled=True, + ), + AguaIOTSensorEntityDescription( + key="real_power_get", + name="Real Power", + icon="mdi:gauge", + native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + force_enabled=True, + raw_value=True, + hybrid_exclude=True, + ), + AguaIOTSensorEntityDescription( + key="real_power_get", + name="Real Pellet Power", + icon="mdi:gauge", + native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + force_enabled=True, + raw_value=True, + hybrid_only=True, + ), + AguaIOTSensorEntityDescription( + key="real_power_wood_get", + name="Real Wood Power", + icon="mdi:gauge", + native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + force_enabled=True, + hybrid_only=True, + raw_value=True, + ), + AguaIOTSensorEntityDescription( + key="vent_front2_get", + name="Real Vent Front", + icon="mdi:fan", + native_unit_of_measurement=None, + state_class=None, + device_class=SensorDeviceClass.ENUM, + ), + AguaIOTSensorEntityDescription( + key="vent_rear2_get", + name="Real Vent Rear", + icon="mdi:fan", + native_unit_of_measurement=None, + state_class=None, + device_class=SensorDeviceClass.ENUM, + ), + AguaIOTSensorEntityDescription( + key="type_combustible_get", + name="Fuel", + icon="mdi:gas-burner", + native_unit_of_measurement=None, + state_class=None, + device_class=SensorDeviceClass.ENUM, + ), + AguaIOTSensorEntityDescription( + key="pres_h2o_get", + name="Water Pressure", + native_unit_of_measurement=UnitOfPressure.BAR, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + ), + AguaIOTSensorEntityDescription( + key="pascal_get", + name="Brazier Pressure", + native_unit_of_measurement=UnitOfPressure.PA, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.PRESSURE, + ), + AguaIOTSensorEntityDescription( + key="giri_estrattore_get", + name="Extractor Fan", + icon="mdi:fan", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + device_class=None, + ), + AguaIOTSensorEntityDescription( + key="pomp_h2o_get", + name="Water Pump", + icon="mdi:pump", + native_unit_of_measurement=None, + state_class=None, + device_class=None, + ), + # Temperature sensors + AguaIOTSensorEntityDescription( + key="temp_gas_flue_get", + name="Smoke Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_probe_k_get", + name="Flame Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_air_get", + name="Air Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_air2_get", + name="Air Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_air3_get", + name="Air Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_air_palm_get", + name="Remote Air Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_rear2_get", + name="Vent Rear Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_front2_get", + name="Vent Front Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_water_get", + name="Water Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_h2o_mandata_get", + name="Water Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_h2o_get", + name="Water Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="temp_legna_get", + name="Wood Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + ), + AguaIOTSensorEntityDescription( + key="pellet_level_get", + name="Pellet Level", + native_unit_of_measurement="%", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:percent", + ), +) + +SWITCHES = ( + SwitchEntityDescription( + key="natural_mode_manual_set", + name="Natural Mode", + icon="mdi:fan-off", + device_class=SwitchDeviceClass.SWITCH, + ), + SwitchEntityDescription( + key="standby_set", + name="Standby", + icon="mdi:power-standby", + device_class=SwitchDeviceClass.SWITCH, + ), + SwitchEntityDescription( + key="fun_auto_set", + name="Auto Mode", + icon="mdi:fan-auto", + device_class=SwitchDeviceClass.SWITCH, + ), + SwitchEntityDescription( + key="fun_pwf_set", + name="Powerful Mode", + icon="mdi:speedometer", + device_class=SwitchDeviceClass.SWITCH, + ), + SwitchEntityDescription( + key="eco_stop_set", + name="ECO Stop", + icon="mdi:leaf-off", + device_class=SwitchDeviceClass.SWITCH, + ), +) + +NUMBERS = ( + AguaIOTNumberEntityDescription( + key="es_air_start_set", + name="Energy Saving Start", + native_step=1, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=NumberDeviceClass.TEMPERATURE, + ), + AguaIOTNumberEntityDescription( + key="es_air_stop_set", + name="Energy Saving Stop", + native_step=1, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=NumberDeviceClass.TEMPERATURE, + ), + AguaIOTNumberEntityDescription( + key="power_set", + name="Power", + icon="mdi:fire", + native_step=1, + force_enabled=True, + hybrid_exclude=True, + ), + AguaIOTNumberEntityDescription( + key="power_set", + name="Pellet Power", + icon="mdi:fire", + native_step=1, + force_enabled=True, + hybrid_only=True, + ), + AguaIOTNumberEntityDescription( + key="power_wood_set", + name="Wood Power", + icon="mdi:fire", + native_step=1, + hybrid_only=True, + ), + AguaIOTNumberEntityDescription( + key="eco_temp_stop", + name="ECO Stop Time", + native_step=1, + native_unit_of_measurement="min", + native_min_value=0, + native_max_value=30, + force_enabled=True, + ), +) + +CLIMATE_CANALIZATIONS = ( + AguaIOTCanalizationEntityDescription( + name="Multifire {id}", + key=r"multifire_(?P\d+)_set", + icon="mdi:fan", + ), + AguaIOTCanalizationEntityDescription( + name="Canalization {id}", + key=r"canalization_(?P[a-zA-Z0-9]+)_set", + key_enable="canalization_{id}_enable", + key2_enable="canalization_2{id}_enable", + key_temp_set="canalization_{id}_temp_air_set", + key_temp_get="canalization_{id}_temp_air_get", + key_temp2_get="canalization_2{id}_temp_air_get", + key_vent_set="canalization_{id}_vent_set", + icon="mdi:fan", + ), + AguaIOTCanalizationEntityDescription( + name="Canalization Single", + key=r"canalization_single_vent_set", + icon="mdi:fan", + ), + AguaIOTCanalizationEntityDescription( + name="Vent {id}", + key=r"vent_(?P(front|rear))_set", + key_enable="vent_{id}2_enable", + key2_enable="vent_{id}2_enable", + key_temp_set="temp_{id}_set", + key_temp_get="temp_{id}_get", + key_temp2_get="temp_{id}2_get", + icon="mdi:fan", + ), + AguaIOTCanalizationEntityDescription( + name="Vent {id}", + key=r"vent_(?!(front|rear))(?P\w+)_set", + key_temp_set="temp_{id}_set", + key_temp_get="temp_{id}_get", + icon="mdi:fan", + ), + AguaIOTCanalizationEntityDescription( + name="Multifire", + key="vent_front_sweetair_set", + icon="mdi:fan", + ), +) + +SELECTS = ( + SelectEntityDescription( + key="fan_mode_set", + name="Fan Mode", + icon="mdi:fan", + ), + SelectEntityDescription( + key="fan2_mode_set", + name="Fan Mode", + icon="mdi:fan", + ), +)