DEVELOPMENT ENVIRONMENT

~liljamo/ha-ouman-eh800

e7f024c4a3eda50653243b132a8387e1f71b73c3 — Jonni Liljamo 11 days ago 084a34e
feat: most of the functionality
M custom_components/ouman_eh800/__init__.py => custom_components/ouman_eh800/__init__.py +49 -12
@@ 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",
        )

A custom_components/ouman_eh800/climate.py => custom_components/ouman_eh800/climate.py +137 -0
@@ 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()

M custom_components/ouman_eh800/config_flow.py => custom_components/ouman_eh800/config_flow.py +0 -2
@@ 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)


M custom_components/ouman_eh800/const.py => custom_components/ouman_eh800/const.py +0 -7
@@ 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_ = ""

M custom_components/ouman_eh800/eh800.py => custom_components/ouman_eh800/eh800.py +119 -42
@@ 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)

M custom_components/ouman_eh800/manifest.json => custom_components/ouman_eh800/manifest.json +2 -1
@@ 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"]
}

A custom_components/ouman_eh800/number.py => custom_components/ouman_eh800/number.py +80 -0
@@ 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()

M custom_components/ouman_eh800/sensor.py => custom_components/ouman_eh800/sensor.py +53 -101
@@ 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()

A custom_components/ouman_eh800/valve.py => custom_components/ouman_eh800/valve.py +109 -0
@@ 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()

M flake.nix => flake.nix +6 -1
@@ 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;


M requirements.dev.txt => requirements.dev.txt +1 -0
@@ 1,1 1,2 @@
homeassistant==2024.10.4
httpx==0.27.2