From e7f024c4a3eda50653243b132a8387e1f71b73c3 Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Tue, 12 Nov 2024 12:57:58 +0200 Subject: [PATCH] feat: most of the functionality --- custom_components/ouman_eh800/__init__.py | 61 +++++-- custom_components/ouman_eh800/climate.py | 137 ++++++++++++++++ custom_components/ouman_eh800/config_flow.py | 2 - custom_components/ouman_eh800/const.py | 7 - custom_components/ouman_eh800/eh800.py | 161 ++++++++++++++----- custom_components/ouman_eh800/manifest.json | 3 +- custom_components/ouman_eh800/number.py | 80 +++++++++ custom_components/ouman_eh800/sensor.py | 154 ++++++------------ custom_components/ouman_eh800/valve.py | 109 +++++++++++++ flake.nix | 7 +- requirements.dev.txt | 1 + 11 files changed, 556 insertions(+), 166 deletions(-) create mode 100644 custom_components/ouman_eh800/climate.py create mode 100644 custom_components/ouman_eh800/number.py create mode 100644 custom_components/ouman_eh800/valve.py diff --git a/custom_components/ouman_eh800/__init__.py b/custom_components/ouman_eh800/__init__.py index 8c4027d..9cbf430 100644 --- a/custom_components/ouman_eh800/__init__.py +++ b/custom_components/ouman_eh800/__init__.py @@ -1,28 +1,65 @@ -""" -ouman_eh800 -""" - +from datetime import timedelta import logging from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.util import Throttle -from .const import DOMAIN +from .const import DOMAIN, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD +from .eh800 import EH800 _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR, Platform.VALVE] +UPDATE_INTERVAL = timedelta(minutes=1) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """TODO""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Setting up Ouman EH-800") - hass.data.setdefault(DOMAIN, {}) - hass_data = dict(entry.data) - hass.data[DOMAIN][entry.entry_id] = hass_data + config = dict(entry.data) + eh800 = EH800( + config[CONF_HOST], + config[CONF_PORT], + config[CONF_USERNAME], + config[CONF_PASSWORD], + ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + device = OumanEH800Device(hass, eh800, entry) + hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: device}) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True + + +class OumanEH800Device: + """Ouman EH-800 Device instance.""" + + def __init__(self, hass: HomeAssistant, device: EH800, entry: ConfigEntry) -> None: + self._hass = hass + self.device = device + self.entry = entry + + self._available = True + + @Throttle(UPDATE_INTERVAL) + async def async_update( + self, + **kwargs, # pylint: disable=unused-argument + ): + """Pull data from Ouman EH-800.""" + + update_ok = await self.device.update() + if not update_ok: + _LOGGER.warning("Failed to update EH-800 device!") + + @property + def device_info(self) -> DeviceInfo: + return DeviceInfo( + identifiers={(DOMAIN, self.entry.entry_id)}, + manufacturer="Ouman", + name="Ouman EH-800", + ) diff --git a/custom_components/ouman_eh800/climate.py b/custom_components/ouman_eh800/climate.py new file mode 100644 index 0000000..669e712 --- /dev/null +++ b/custom_components/ouman_eh800/climate.py @@ -0,0 +1,137 @@ +import logging + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityDescription, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import OumanEH800Device +from .const import DOMAIN +from .eh800 import OPERATION_MODES + +_LOGGER = logging.getLogger(__name__) + + +class OumanEH800DeviceClimateEntityDescription( + ClimateEntityDescription, frozen_or_thawed=True +): # pylint: disable=too-few-public-methods + current_temperature_key: str + target_temperature_key: str + operation_mode_key: str + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Ouman EH-800 device climate control.""" + device = hass.data[DOMAIN].get(entry.entry_id) + + entities: list[OumanEH800DeviceClimate] = [ + OumanEH800DeviceClimate( + device, + OumanEH800DeviceClimateEntityDescription( + key="l1_climate", + current_temperature_key="l1_room_temperature", + target_temperature_key="l1_target_room_temperature", + operation_mode_key="l1_operation_mode", + ), + ) + ] + + async_add_entities(entities, True) + + +class OumanEH800DeviceClimate(ClimateEntity): + entity_description: OumanEH800DeviceClimateEntityDescription + + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ) + _attr_temperature_unit = UnitOfTemperature.CELSIUS + + def __init__( + self, + device: OumanEH800Device, + description: OumanEH800DeviceClimateEntityDescription, + ) -> None: + self._device = device + self.entity_description = description + + self._attr_name = description.key.replace("_", " ").capitalize() + self._attr_unique_id = f"ouman_eh800_{description.key}" + self._attr_device_info = device.device_info + + @property + def extra_state_attributes(self) -> dict: + return self._device.device.data + + @property + def hvac_mode(self) -> HVACMode: + operation_mode = int( + self._device.device.data.get(self.entity_description.operation_mode_key, 0) + ) + if operation_mode == 5: + return HVACMode.OFF + if operation_mode == 0: + return HVACMode.AUTO + return HVACMode.HEAT + + @property + def hvac_modes(self) -> list[HVACMode]: + return [] + + @property + def preset_mode(self) -> str: + operation_mode = int( + self._device.device.data.get(self.entity_description.operation_mode_key, 0) + ) + return [om.name for om in OPERATION_MODES if om.value == operation_mode][0] + + @property + def preset_modes(self) -> list[str]: + return [om.name for om in OPERATION_MODES] + + @property + def current_temperature(self) -> float: + return float( + self._device.device.data.get( + self.entity_description.current_temperature_key, 0.0 + ) + ) + + @property + def target_temperature(self) -> float: + return float( + self._device.device.data.get( + self.entity_description.target_temperature_key, 0.0 + ) + ) + + async def async_set_temperature(self, **kwargs) -> None: + await self._device.device.update_value( + self.entity_description.target_temperature_key, + kwargs.get("temperature", self.target_temperature), + ) + self.async_write_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + operation_mode = [om for om in OPERATION_MODES if om.name == preset_mode][0] + _LOGGER.debug( + "Setting operation mode to '%s' (%s)", + operation_mode.name, + operation_mode.value, + ) + await self._device.device.update_value( + self.entity_description.operation_mode_key, + operation_mode.value, + ) + self.async_write_ha_state() + + async def async_update(self) -> None: + await self._device.async_update() diff --git a/custom_components/ouman_eh800/config_flow.py b/custom_components/ouman_eh800/config_flow.py index bf3d704..63dc8a9 100644 --- a/custom_components/ouman_eh800/config_flow.py +++ b/custom_components/ouman_eh800/config_flow.py @@ -39,7 +39,6 @@ class OumanEH800ConfigFlow( async def _create_entry( self, host: str, port: int, username: str, password: str ) -> ConfigFlowResult: - """Register new entry.""" return self.async_create_entry( title=f"Ouman {host}", data={ @@ -53,7 +52,6 @@ class OumanEH800ConfigFlow( async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """TODO""" if user_input is None: return self.async_show_form(step_id="user", data_schema=USER_SCHEMA) diff --git a/custom_components/ouman_eh800/const.py b/custom_components/ouman_eh800/const.py index ce1f193..e1f0f04 100644 --- a/custom_components/ouman_eh800/const.py +++ b/custom_components/ouman_eh800/const.py @@ -8,10 +8,3 @@ CONF_HOST = "host" CONF_PORT = "port" CONF_USERNAME = "username" CONF_PASSWORD = "password" - -TEMPERATURE_SENSOR_TYPE_OUTSIDE = "outside" -TEMPERATURE_SENSOR_TYPE_L1_ROOM = "l1_room" -TEMPERATURE_SENSOR_TYPE_L1_SUPPLY = "l1_supply" -# TEMPERATURE_SENSOR_TYPE_ = "" -# TEMPERATURE_SENSOR_TYPE_ = "" -# TEMPERATURE_SENSOR_TYPE_ = "" diff --git a/custom_components/ouman_eh800/eh800.py b/custom_components/ouman_eh800/eh800.py index 13c140e..6f66a75 100644 --- a/custom_components/ouman_eh800/eh800.py +++ b/custom_components/ouman_eh800/eh800.py @@ -1,64 +1,141 @@ -"""TODO""" - +import dataclasses import logging -import requests + +from httpx import AsyncClient _LOGGER = logging.getLogger(__name__) -class EH800: - """TODO""" +@dataclasses.dataclass(frozen=True, kw_only=True) +class Value: + key: str + register: str + + +VALUES: tuple[Value, ...] = ( + # Asetusarvot > Menoveden minimiraja + Value(key="l1_supply_temperature_minimum", register="S_54_85"), + # Asetusarvot > Menoveden maksimiraja + Value(key="l1_supply_temperature_maximum", register="S_55_85"), + # Asetusarvot > L1 Säätökäyrä + # n = negative, p = positive + Value(key="l1_heating_curve_n_20", register="S_67_85"), + Value(key="l1_heating_curve_n_10", register="S_69_85"), + Value(key="l1_heating_curve_zero", register="S_71_85"), + Value(key="l1_heating_curve_p_10", register="S_73_85"), + Value(key="l1_heating_curve_p_20", register="S_75_85"), + # Asetusarvot > Huonelämpötila + Value(key="l1_target_room_temperature", register="S_81_85"), + # Asetusarvot > Lämmönpudotus (huonelämpö) + Value(key="l1_temperature_drop", register="S_87_85"), + # Asetusarvot > Suuri lämmönpudotus (huonelämpö) + Value(key="l1_temperature_drop_big", register="S_88_85"), + # Ohjaustavat + # 0 = Automaatti + # 3 = Pakko-ohjaus, norm. lämpötaso + # 1 = Pakko-ohjaus, lämmönpudotus + # 2 = Pakko-ohjaus, suuri lämmönpudotus + # 6 = Käsiajo, sähköinen + # 5 = Alasajo + Value(key="l1_operation_mode", register="S_59_85"), + Value(key="l1_manual_drive_valve_position", register="S_92_85"), + # Mittaukset > Ulkolämpötila + Value(key="outside_temperature", register="S_227_85"), + # Mittaukset > L1 Menoveden lämpötila + Value(key="l1_supply_temperature", register="S_259_85"), + # Mittaukset > L1 Huonelämpötila + Value(key="l1_room_temperature", register="S_284_85"), + # Mittaukset > Huonelämpökaukoasetus TMR/SP + Value(key="l1_tmrsp", register="S_274_85"), + # Mittaukset > L1 Venttiilin asento + Value(key="l1_valve_position", register="S_272_85"), + # EH-800 > Huonelämpötila + Value(key="room_temperature", register="S_261_85"), + # EH-800 > Huonelämpötilan hienosäätö + Value(key="room_temperature_fine_adjustment", register="S_102_85"), + # EH-800 > Lämpötaso:: (UI placement is strange) + Value(key="l1_operation_mode_str", register="S_1000_0"), +) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class OperationMode: + name: str + value: int + + +OPERATION_MODES: tuple[OperationMode, ...] = ( + OperationMode(name="Automatic", value=0), + OperationMode(name="Forced - Normal", value=3), + OperationMode(name="Forced - Drop", value=1), + OperationMode(name="Forced - Big Drop", value=2), + OperationMode(name="Manual", value=6), + OperationMode(name="Off", value=5), +) + +class EH800: def __init__(self, host: str, port: int, username: str, password: str) -> None: - """TODO""" self._uri = f"http://{host}:{port}" self._login = f"uid={username};pwd={password};" - self._outside_temp = 0.0 - self._l1_room_temp = 0.0 - self._l1_supply_temp = 0.0 + self._client = AsyncClient() + + self.data = {} + + async def _refresh_login(self) -> bool: + """ + Refresh login. - def _refresh_login(self) -> bool: - """TODO""" - r = requests.get(f"{self._uri}/login?{self._login}") + Logs an error if the login failed. + """ + r = await self._client.get(f"{self._uri}/login?{self._login}") if r.text[:-1] == "login?result=ok;": - _LOGGER.debug("Login ok") return True - _LOGGER.debug("Login error") + _LOGGER.error("Login error") return False - def _request_value(self, register) -> str: - """TODO""" - if not self._refresh_login(): - return "" - - r = requests.get(f"{self._uri}/request?{register}") + async def _request_value(self, register) -> str: + """Request a value from the API.""" + r = await self._client.get(f"{self._uri}/request?{register}") eq_index = r.text.find("=") sc_index = r.text.find(";") return r.text[eq_index + 1 : sc_index] - def get_outside_temp(self) -> float: - """TODO""" - return self._outside_temp - - def update_outside_temp(self): - """TODO""" - self._outside_temp = self._request_value("S_227_85") - - def get_l1_room_temp(self) -> float: - """TODO""" - return self._l1_room_temp - - def update_l1_room_temp(self): - """TODO""" - self._l1_room_temp = self._request_value("S_261_85") + async def _update_value(self, value: Value, new_value) -> None: + """ + Update a value via the API. - def get_l1_supply_temp(self) -> float: - """TODO""" - return self._l1_supply_temp - - def update_l1_supply_temp(self): - """TODO""" - self._l1_supply_temp = self._request_value("S_259_85") + Checks the API return value and logs an error if the value update failed. + """ + r = await self._client.get(f"{self._uri}/update?{value.register}={new_value};") + eq_index = r.text.find("=") + sc_index = r.text.find(";") + got_value = r.text[eq_index + 1 : sc_index] + if str(got_value) != str(new_value): + _LOGGER.error( + "Value update failed, got '%s', wanted '%s'", got_value, new_value + ) + else: + # Update data to match the new value + self.data[value.key] = new_value + + async def update(self) -> bool: + """Update data values from the API.""" + if not await self._refresh_login(): + return False + + for value in VALUES: + self.data[value.key] = await self._request_value(value.register) + + return True + + async def update_value(self, key, new_value) -> None: + """Update a value via the API.""" + value = [value for value in VALUES if value.key == key][0] + _LOGGER.debug( + "Updating '%s' (%s) to '%s'", value.key, value.register, new_value + ) + await self._update_value(value, new_value) diff --git a/custom_components/ouman_eh800/manifest.json b/custom_components/ouman_eh800/manifest.json index a267b59..ca66a97 100644 --- a/custom_components/ouman_eh800/manifest.json +++ b/custom_components/ouman_eh800/manifest.json @@ -2,5 +2,6 @@ "domain": "ouman_eh800", "name": "Ouman EH-800", "config_flow": true, - "version": "0.1.0" + "version": "0.1.0", + "requirements": ["httpx==0.27.2"] } diff --git a/custom_components/ouman_eh800/number.py b/custom_components/ouman_eh800/number.py new file mode 100644 index 0000000..bbaf037 --- /dev/null +++ b/custom_components/ouman_eh800/number.py @@ -0,0 +1,80 @@ +import logging + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import OumanEH800Device +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +NUMBERS: tuple[str, ...] = ( + "l1_temperature_drop", + "l1_temperature_drop_big", +) +NUMBERS: tuple[NumberEntityDescription, ...] = ( + NumberEntityDescription( + key="l1_temperature_drop", + device_class=NumberDeviceClass.TEMPERATURE, + mode="box", + native_max_value=90.0, + native_min_value=0.0, + native_step=0.5, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + NumberEntityDescription( + key="l1_temperature_drop_big", + device_class=NumberDeviceClass.TEMPERATURE, + mode="box", + native_max_value=90.0, + native_min_value=0.0, + native_step=0.5, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Ouman EH-800 device numbers.""" + device = hass.data[DOMAIN].get(entry.entry_id) + + entities: list[OumanEH800DeviceNumber] = [ + OumanEH800DeviceNumber(device, description) for description in NUMBERS + ] + + async_add_entities(entities, True) + + +class OumanEH800DeviceNumber(NumberEntity): + entity_description: NumberEntityDescription + + def __init__( + self, device: OumanEH800Device, description: NumberEntityDescription + ) -> None: + self._device = device + self.entity_description = description + + self._attr_name = description.key.replace("_", " ").capitalize() + self._attr_unique_id = f"ouman_eh800_{description.key}" + self._attr_device_info = device.device_info + + @property + def native_value(self) -> float: + return self._device.device.data.get(self.entity_description.key, 0.0) + + async def async_set_native_value(self, value: float) -> None: + await self._device.device.update_value(self.entity_description.key, value) + self.async_write_ha_state() + + async def async_update(self) -> None: + await self._device.async_update() diff --git a/custom_components/ouman_eh800/sensor.py b/custom_components/ouman_eh800/sensor.py index 3725360..d64267d 100644 --- a/custom_components/ouman_eh800/sensor.py +++ b/custom_components/ouman_eh800/sensor.py @@ -1,121 +1,73 @@ -""" -TODO -""" - import logging -from datetime import timedelta - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) -from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import Throttle -from .const import ( - DOMAIN, - CONF_HOST, - CONF_PORT, - CONF_USERNAME, - CONF_PASSWORD, - TEMPERATURE_SENSOR_TYPE_L1_SUPPLY, - TEMPERATURE_SENSOR_TYPE_L1_ROOM, - TEMPERATURE_SENSOR_TYPE_OUTSIDE, -) -from .eh800 import EH800 +from . import OumanEH800Device +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """TODO""" - config = hass.data[DOMAIN][entry.entry_id] - - eh800 = EH800( - config[CONF_HOST], - config[CONF_PORT], - config[CONF_USERNAME], - config[CONF_PASSWORD], - ) - - entities = [] - entities.append(TemperatureSensor(hass, eh800, TEMPERATURE_SENSOR_TYPE_OUTSIDE)) - entities.append(TemperatureSensor(hass, eh800, TEMPERATURE_SENSOR_TYPE_L1_ROOM)) - entities.append(TemperatureSensor(hass, eh800, TEMPERATURE_SENSOR_TYPE_L1_SUPPLY)) - - async_add_entities(entities) - - return True - - -class TemperatureSensor(SensorEntity): - """Temperature sensor""" - - def __init__(self, hass: HomeAssistant, api: EH800, sensor_type: str): - self._hass = hass - self._api = api - self._sensor_type = sensor_type - - self._unique_id = f"{DOMAIN}_temperature_{sensor_type}".lower() - - self._state = 0.0 - - @property - def device_class(self): - """TODO""" - return SensorDeviceClass.TEMPERATURE - - @property - def device_info(self): - """TODO""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - } - - @property - def name(self): - """TODO""" - return self.unique_id - - @property - def native_unit_of_measurement(self): - """TODO""" - return UnitOfTemperature.CELSIUS +TEMPERATURE_SENSORS: tuple[str, ...] = ( + "outside_temperature", + "l1_supply_temperature", + "l1_room_temperature", + "l1_tmrsp", +) - @property - def state(self): - """TODO""" - return self._state - @property - def state_class(self): - """TODO""" - return SensorStateClass.MEASUREMENT +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Ouman EH-800 device sensors.""" + device = hass.data[DOMAIN].get(entry.entry_id) + + entities: list[OumanEH800DeviceSensor] = [ + OumanEH800DeviceSensor( + device, + sensor, + SensorEntityDescription( + key=sensor, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ) + for sensor in TEMPERATURE_SENSORS + ] + + async_add_entities(entities, True) + + +class OumanEH800DeviceSensor(SensorEntity): + entity_description: SensorEntityDescription + + def __init__( + self, + device: OumanEH800Device, + value_key: str, + description: SensorEntityDescription, + ) -> None: + self._device = device + self._value_key = value_key + self.entity_description = description + + self._attr_name = description.key.replace("_", " ").capitalize() + self._attr_unique_id = f"ouman_eh800_{description.key}" + self._attr_device_info = device.device_info @property - def unique_id(self): - """TODO""" - return self._unique_id + def native_value(self) -> float: + return self._device.device.data.get(self._value_key, 0.0) - @Throttle(timedelta(minutes=1)) - async def async_update(self): - """TODO""" - if self._sensor_type == TEMPERATURE_SENSOR_TYPE_OUTSIDE: - await self._hass.async_add_executor_job(self._api.update_outside_temp) - self._state = self._api.get_outside_temp() - elif self._sensor_type == TEMPERATURE_SENSOR_TYPE_L1_ROOM: - await self._hass.async_add_executor_job(self._api.update_l1_room_temp) - self._state = self._api.get_l1_room_temp() - elif self._sensor_type == TEMPERATURE_SENSOR_TYPE_L1_SUPPLY: - await self._hass.async_add_executor_job(self._api.update_l1_supply_temp) - self._state = self._api.get_l1_supply_temp() + async def async_update(self) -> None: + await self._device.async_update() diff --git a/custom_components/ouman_eh800/valve.py b/custom_components/ouman_eh800/valve.py new file mode 100644 index 0000000..34a6387 --- /dev/null +++ b/custom_components/ouman_eh800/valve.py @@ -0,0 +1,109 @@ +import logging + +from homeassistant.components.valve import ( + ValveDeviceClass, + ValveEntity, + ValveEntityDescription, + ValveEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import OumanEH800Device +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +VALVES_RO: tuple[str, ...] = ("l1_valve_position",) + +VALVES_RW: tuple[str, ...] = ("l1_manual_drive_valve_position",) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Ouman EH-800 device valves.""" + device = hass.data[DOMAIN].get(entry.entry_id) + + entities: list[OumanEH800DeviceValve] = [] + + entities.extend( + [ + OumanEH800DeviceValveRO( + device, + valve, + ValveEntityDescription( + key=valve, + device_class=ValveDeviceClass.WATER, + ), + ) + for valve in VALVES_RO + ] + ) + + entities.extend( + [ + OumanEH800DeviceValveRW( + device, + valve, + ValveEntityDescription( + key=valve, + device_class=ValveDeviceClass.WATER, + ), + ) + for valve in VALVES_RW + ] + ) + + async_add_entities(entities, True) + + +class OumanEH800DeviceValve(ValveEntity): + entity_description: ValveEntityDescription + + def __init__( + self, + device: OumanEH800Device, + value_key: str, + description: ValveEntityDescription, + ) -> None: + self._device = device + self._value_key = value_key + self.entity_description = description + + self._attr_name = description.key.replace("_", " ").capitalize() + self._attr_unique_id = f"ouman_eh800_{description.key}" + self._attr_device_info = device.device_info + + @property + def current_valve_position(self) -> int: + return int(self._device.device.data.get(self._value_key, 0)) + + @property + def reports_position(self) -> bool: + return True + + async def async_update(self) -> None: + await self._device.async_update() + + +class OumanEH800DeviceValveRO(OumanEH800DeviceValve): + """A valve that can only be read.""" + + +class OumanEH800DeviceValveRW(OumanEH800DeviceValve): + """ + A valve that can be read and set. + + Supports setting the position of the valve, and closing the valve. + """ + + _attr_supported_features = ( + ValveEntityFeature.CLOSE | ValveEntityFeature.SET_POSITION + ) + + async def async_set_valve_position(self, position: int) -> None: + await self._device.device.update_value(self.entity_description.key, position) + self.async_write_ha_state() diff --git a/flake.nix b/flake.nix index ad4e780..58f5509 100644 --- a/flake.nix +++ b/flake.nix @@ -37,7 +37,12 @@ # Python linting and formatting pylint = { enable = true; - args = ["--disable=import-error"]; + args = [ + "--disable=import-error" + "--disable=missing-class-docstring" + "--disable=missing-function-docstring" + "--disable=missing-module-docstring" + ]; }; black.enable = true; diff --git a/requirements.dev.txt b/requirements.dev.txt index 2d65e9e..f825e6c 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1 +1,2 @@ homeassistant==2024.10.4 +httpx==0.27.2 -- 2.44.1