Skip to content
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ homeassistant.components.opower.*
homeassistant.components.oralb.*
homeassistant.components.otbr.*
homeassistant.components.otp.*
homeassistant.components.ouman_eh_800.*
homeassistant.components.overkiz.*
homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions homeassistant/components/ouman_eh_800/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""The Ouman EH-800 integration."""

from __future__ import annotations

Check failure on line 3 in homeassistant/components/ouman_eh_800/__init__.py

View workflow job for this annotation

GitHub Actions / Run prek checks

ruff (TID251)

homeassistant/components/ouman_eh_800/__init__.py:3:24: TID251 `__future__.annotations` is banned: It should not be needed because Home Assistant requires Python 3.14+

from homeassistant.const import Platform
from homeassistant.core import HomeAssistant

from .coordinator import OumanEh800ConfigEntry, OumanEh800Coordinator

_PLATFORMS: list[Platform] = [
Platform.SENSOR,
]


async def async_setup_entry(hass: HomeAssistant, entry: OumanEh800ConfigEntry) -> bool:
"""Set up Ouman EH-800 from a config entry."""
coordinator = OumanEh800Coordinator(hass, entry)

await coordinator.async_config_entry_first_refresh()

entry.runtime_data = coordinator

await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: OumanEh800ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
77 changes: 77 additions & 0 deletions homeassistant/components/ouman_eh_800/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Config flow for the Ouman EH-800 integration."""

from __future__ import annotations

Check failure on line 3 in homeassistant/components/ouman_eh_800/config_flow.py

View workflow job for this annotation

GitHub Actions / Run prek checks

ruff (TID251)

homeassistant/components/ouman_eh_800/config_flow.py:3:24: TID251 `__future__.annotations` is banned: It should not be needed because Home Assistant requires Python 3.14+

import logging
from typing import Any

from ouman_eh_800_api import (
OumanClientAuthenticationError,
OumanClientCommunicationError,
OumanEh800Client,
)
import voluptuous as vol
from yarl import URL

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)


def _normalize_url(url: str) -> str:
"""Reduce URL to scheme://host[:port], discarding any path, query, or fragment."""
return str(URL(url.strip()).origin())


class OumanEh800ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ouman EH-800."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
user_input[CONF_URL] = _normalize_url(user_input[CONF_URL])
except ValueError:
errors[CONF_URL] = "invalid_url"
else:
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
Comment thread
Markus98 marked this conversation as resolved.
client = OumanEh800Client(
session=async_get_clientsession(self.hass),
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
address=user_input[CONF_URL],
)
try:
await client.login()
except OumanClientCommunicationError:
errors["base"] = "cannot_connect"
except OumanClientAuthenticationError:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title="Ouman EH-800", data=user_input
)
Comment thread
Markus98 marked this conversation as resolved.

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
15 changes: 15 additions & 0 deletions homeassistant/components/ouman_eh_800/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Constants for the Ouman EH-800 integration."""

from enum import StrEnum

DOMAIN = "ouman_eh_800"
Comment thread
Markus98 marked this conversation as resolved.

DEFAULT_SCAN_INTERVAL_SECONDS = 60


class OumanDevice(StrEnum):
"""Logical device that an entity belongs to."""

MAIN = "main"
L1 = "l1"
L2 = "l2"
94 changes: 94 additions & 0 deletions homeassistant/components/ouman_eh_800/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Data update coordinator for the Ouman EH-800 integration."""

from datetime import timedelta
import logging

from ouman_eh_800_api import (
OumanClientAuthenticationError,
OumanClientCommunicationError,
OumanEh800Client,
OumanEndpoint,
OumanRegistrySet,
OumanValues,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DEFAULT_SCAN_INTERVAL_SECONDS, DOMAIN, OumanDevice

_LOGGER = logging.getLogger(__name__)

type OumanEh800ConfigEntry = ConfigEntry[OumanEh800Coordinator]


class OumanEh800Coordinator(DataUpdateCoordinator[dict[OumanEndpoint, OumanValues]]):
"""Ouman EH-800 data update coordinator."""

_registry_set: OumanRegistrySet

def __init__(
self,
hass: HomeAssistant,
config_entry: OumanEh800ConfigEntry,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name="Ouman EH-800",
config_entry=config_entry,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL_SECONDS),
always_update=False,
)
self.client: OumanEh800Client = OumanEh800Client(
session=async_get_clientsession(hass),
username=config_entry.data[CONF_USERNAME],
password=config_entry.data[CONF_PASSWORD],
address=config_entry.data[CONF_URL],
)

entry_id = config_entry.entry_id
main_device_identifier = (DOMAIN, entry_id)
self.device_info: dict[OumanDevice, DeviceInfo] = {
OumanDevice.MAIN: DeviceInfo(
identifiers={main_device_identifier},
name="Ouman EH-800",
Comment thread
Markus98 marked this conversation as resolved.
manufacturer="Ouman",
model="EH-800",
configuration_url=config_entry.data[CONF_URL],
),
OumanDevice.L1: DeviceInfo(
identifiers={(DOMAIN, f"{entry_id}_{OumanDevice.L1}")},
translation_key=OumanDevice.L1,
via_device=main_device_identifier,
),
OumanDevice.L2: DeviceInfo(
identifiers={(DOMAIN, f"{entry_id}_{OumanDevice.L2}")},
translation_key=OumanDevice.L2,
via_device=main_device_identifier,
),
}

async def _async_setup(self) -> None:
try:
# Even though not required to fetch values, perform login once
# at the start to verify that the credentials are valid.
await self.client.login()
self._registry_set = await self.client.get_active_registries()
except OumanClientAuthenticationError as err:
raise ConfigEntryError("Invalid credentials") from err
except OumanClientCommunicationError as err:
raise ConfigEntryNotReady("Error communicating with API") from err

Comment thread
Markus98 marked this conversation as resolved.
async def _async_update_data(self) -> dict[OumanEndpoint, OumanValues]:
"""Fetch registry values from the device."""
try:
return await self.client.get_values(self._registry_set)
except OumanClientCommunicationError as err:
raise UpdateFailed("Error communicating with API") from err
Comment thread
Markus98 marked this conversation as resolved.
43 changes: 43 additions & 0 deletions homeassistant/components/ouman_eh_800/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Base entity for Ouman EH-800."""

from dataclasses import dataclass

from ouman_eh_800_api import OumanEndpoint

from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import OumanDevice
from .coordinator import OumanEh800Coordinator


@dataclass(frozen=True, kw_only=True)
class OumanEh800EntityDescription(EntityDescription):
"""Common Ouman EH-800 entity description fields."""

device: OumanDevice


class OumanEh800Entity(CoordinatorEntity[OumanEh800Coordinator]):
"""Base entity for Ouman EH-800."""

_attr_has_entity_name = True
entity_description: OumanEh800EntityDescription

def __init__(
self,
coordinator: OumanEh800Coordinator,
endpoint: OumanEndpoint,
description: OumanEh800EntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._endpoint = endpoint
self.entity_description = description

assert coordinator.config_entry is not None
Comment thread
Markus98 marked this conversation as resolved.
self._attr_unique_id = (
f"{coordinator.config_entry.entry_id}"
f"_{description.device}_{description.key}"
)
self._attr_device_info = coordinator.device_info[description.device]
9 changes: 9 additions & 0 deletions homeassistant/components/ouman_eh_800/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"entity": {
"sensor": {
"valve_position": {
"default": "mdi:pipe-valve"
}
}
}
}
11 changes: 11 additions & 0 deletions homeassistant/components/ouman_eh_800/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "ouman_eh_800",
"name": "Ouman EH-800",
"codeowners": ["@Markus98"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ouman_eh_800",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["ouman-eh-800-api==0.5.0"]
}
76 changes: 76 additions & 0 deletions homeassistant/components/ouman_eh_800/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not provide actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not provide actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Integration does not use events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done

# Silver
action-exceptions:
status: exempt
comment: Integration does not provide actions.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo

# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: Integration is local polling only, no discovery.
discovery:
status: exempt
comment: Integration is local polling only, no discovery.
Comment thread
Markus98 marked this conversation as resolved.
docs-data-update: done
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: Integration supports a single device per config entry.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: exempt
comment: Integration supports a single device per config entry.

# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
Loading
Loading