diff --git a/custom_components/aguaiot/select.py b/custom_components/aguaiot/select.py new file mode 100644 index 0000000..e1c6015 --- /dev/null +++ b/custom_components/aguaiot/select.py @@ -0,0 +1,78 @@ +import logging +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.select import SelectEntity +from homeassistant.helpers.entity import DeviceInfo +from .const import SELECTS, 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 + + selects = [] + for device in agua.devices: + for select in SELECTS: + if select.key in device.registers and device.get_register_enabled( + select.key + ): + selects.append(AguaIOTHeatingSelect(coordinator, device, select)) + + async_add_entities(selects, True) + + +class AguaIOTHeatingSelect(CoordinatorEntity, SelectEntity): + """Select 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 current_option(self): + return self._device.get_register_value_description(self.entity_description.key) + + @property + def options(self): + return list( + self._device.get_register_value_options( + self.entity_description.key + ).values() + ) + + async def async_select_option(self, option): + try: + await self._device.set_register_value_description( + self.entity_description.key, option + ) + await self.coordinator.async_request_refresh() + except (ValueError, AguaIOTError) as err: + _LOGGER.error("Failed to set value, error: %s", err) diff --git a/custom_components/aguaiot/sensor.py b/custom_components/aguaiot/sensor.py new file mode 100644 index 0000000..e607539 --- /dev/null +++ b/custom_components/aguaiot/sensor.py @@ -0,0 +1,110 @@ +import numbers +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.sensor import SensorEntity, SensorDeviceClass +from homeassistant.helpers.entity import DeviceInfo +from .const import 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 SENSORS: + if ( + sensor.key in device.registers + and (sensor.force_enabled or device.get_register_enabled(sensor.key)) + and ( + (sensor.hybrid_only and hybrid) + or (sensor.hybrid_exclude and not hybrid) + or (not sensor.hybrid_only and not sensor.hybrid_exclude) + ) + ): + sensors.append(AguaIOTHeatingSensor(coordinator, device, sensor)) + + async_add_entities(sensors, True) + + +class AguaIOTHeatingSensor(CoordinatorEntity, SensorEntity): + """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 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.""" + if self.entity_description.raw_value: + return self._device.get_register_value(self.entity_description.key) + else: + value = self._device.get_register_value_description( + self.entity_description.key + ) + # Do not return a description if the sensor expects a number + if not self.entity_description.native_unit_of_measurement or isinstance( + value, numbers.Number + ): + return value + + @property + def extra_state_attributes(self): + """Expose plain value as extra attribute when needed.""" + if ( + not self.entity_description.raw_value + and self._device.get_register_value_options(self.entity_description.key) + ): + return { + "raw_value": self._device.get_register_value( + self.entity_description.key + ), + } + + @property + def options(self): + if self.entity_description.device_class == SensorDeviceClass.ENUM: + options = sorted( + list( + set( + self._device.get_register_value_options( + self.entity_description.key + ).values() + ) + ) + ) + cur_value = self._device.get_register_value_description( + self.entity_description.key + ) + if cur_value not in options: + options.append(cur_value) + + return options diff --git a/custom_components/aguaiot/services.yaml b/custom_components/aguaiot/services.yaml new file mode 100644 index 0000000..e9b18c0 --- /dev/null +++ b/custom_components/aguaiot/services.yaml @@ -0,0 +1,4 @@ +sync_clock: + target: + entity: + domain: climate diff --git a/custom_components/aguaiot/strings.json b/custom_components/aguaiot/strings.json new file mode 100644 index 0000000..4b169d7 --- /dev/null +++ b/custom_components/aguaiot/strings.json @@ -0,0 +1,47 @@ +{ + "config": { + "flow_title": "Micronova Agua IOT", + "step": { + "user": { + "description": "[%key:common::config_flow::description%]", + "data": { + "api_url": "[%key:common::config_flow::data::api_url%]", + "login_api_url": "[%key:common::config_flow::data::login_api_url%]", + "customer_code": "[%key:common::config_flow::data::customer_code%]", + "brand_id": "[%key:common::config_flow::data::brand_id%]", + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "unauthorized": "[%key:common::config_flow::error::unauthorized%]", + "connection_error": "[%key:common::config_flow::error::connection_error%]", + "unknown_error": "[%key:common::config_flow::error::unknown_error%]" + }, + "abort": { + "device_already_configured": "[%key:common::config_flow::abort::device_already_configured%]" + } + }, + "services": { + "sync_clock": { + "name": "Synchronize Stove Clock", + "description": "Synchronize stove time and date with the current Home Assistant time and date." + } + }, + "options": { + "step": { + "user": { + "title": "Micronova Agua IOT options", + "data": { + "air_temp_fix": "[Jolly Mec] Ignore incorrect air temperature when using external themostat.", + "reading_error_fix": "[Bronpi] Filter incorrect readings (when stove is without power).", + "http_timeout": "Timeout for API calls (seconds).", + "buffer_read_timeout": "Timeout for stove buffer reading (seconds).", + "language": "Language for descriptions.", + "update_interval": "Time between updates (seconds)." + } + } + } + } +} diff --git a/custom_components/aguaiot/switch.py b/custom_components/aguaiot/switch.py new file mode 100644 index 0000000..e4d2170 --- /dev/null +++ b/custom_components/aguaiot/switch.py @@ -0,0 +1,86 @@ +import logging +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) +from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers.entity import DeviceInfo +from .const import SWITCHES, 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 + + switches = [] + for device in agua.devices: + for switch in SWITCHES: + if switch.key in device.registers and device.get_register_enabled( + switch.key + ): + switches.append(AguaIOTHeatingSwitch(coordinator, device, switch)) + + async_add_entities(switches, True) + + +class AguaIOTHeatingSwitch(CoordinatorEntity, SwitchEntity): + """Switch 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 f"{self._device.name} {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 is_on(self): + """Return the state of the sensor.""" + return bool(self._device.get_register_value(self.entity_description.key)) + + async def async_turn_off(self): + """Turn device off.""" + try: + await self._device.set_register_value(self.entity_description.key, 0) + await self.coordinator.async_request_refresh() + except AguaIOTError as err: + _LOGGER.error( + "Failed to turn off '%s', error: %s", + f"{self._device.name} {self.entity_description.name}", + err, + ) + + async def async_turn_on(self): + """Turn device on.""" + try: + await self._device.set_register_value(self.entity_description.key, 1) + await self.coordinator.async_request_refresh() + except AguaIOTError as err: + _LOGGER.error( + "Failed to turn on '%s', error: %s", + f"{self._device.name} {self.entity_description.name}", + err, + )