Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add webmin integration #106976

Merged
merged 20 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,7 @@ omit =
homeassistant/components/weatherflow/__init__.py
homeassistant/components/weatherflow/const.py
homeassistant/components/weatherflow/sensor.py
homeassistant/components/webmin/sensor.py
homeassistant/components/wiffi/__init__.py
homeassistant/components/wiffi/binary_sensor.py
homeassistant/components/wiffi/sensor.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,8 @@ build.json @home-assistant/supervisor
/tests/components/weatherkit/ @tjhorner
/homeassistant/components/webhook/ @home-assistant/core
/tests/components/webhook/ @home-assistant/core
/homeassistant/components/webmin/ @autinerd
/tests/components/webmin/ @autinerd
/homeassistant/components/webostv/ @thecode
/tests/components/webostv/ @thecode
/homeassistant/components/websocket_api/ @home-assistant/core
Expand Down
30 changes: 30 additions & 0 deletions homeassistant/components/webmin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""The Webmin integration."""

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

from .const import DOMAIN
from .coordinator import WebminUpdateCoordinator

PLATFORMS = [Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Webmin from a config entry."""

coordinator = WebminUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
await coordinator.async_setup()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
95 changes: 95 additions & 0 deletions homeassistant/components/webmin/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Config flow for Webmin."""
from __future__ import annotations

from collections.abc import Mapping
from http import HTTPStatus
from typing import Any, cast
from xmlrpc.client import Fault

from aiohttp.client_exceptions import ClientConnectionError, ClientResponseError
import voluptuous as vol

from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.helpers import selector
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaConfigFlowHandler,
SchemaFlowError,
SchemaFlowFormStep,
)

from .const import DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN
from .helpers import get_instance_from_options, get_sorted_mac_addresses


async def validate_user_input(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate user input."""
# pylint: disable-next=protected-access
handler.parent_handler._async_abort_entries_match(
{CONF_HOST: user_input[CONF_HOST]}
)
autinerd marked this conversation as resolved.
Show resolved Hide resolved
instance, _ = get_instance_from_options(handler.parent_handler.hass, user_input)
try:
data = await instance.update()
except ClientResponseError as err:
if err.status == HTTPStatus.UNAUTHORIZED:
raise SchemaFlowError("invalid_auth") from err
raise SchemaFlowError("cannot_connect") from err
except Fault as fault:
raise SchemaFlowError(
f"Fault {fault.faultCode}: {fault.faultString}"
) from fault
except ClientConnectionError as err:
raise SchemaFlowError("cannot_connect") from err
except Exception as err:
raise SchemaFlowError("unknown") from err

await cast(SchemaConfigFlowHandler, handler.parent_handler).async_set_unique_id(
get_sorted_mac_addresses(data)[0]
)
return user_input


CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): selector.TextSelector(),
vol.Required(CONF_PORT, default=DEFAULT_PORT): selector.NumberSelector(
selector.NumberSelectorConfig(
min=1, max=65535, mode=selector.NumberSelectorMode.BOX
)
),
vol.Required(CONF_USERNAME): selector.TextSelector(),
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(CONF_SSL, default=DEFAULT_SSL): selector.BooleanSelector(),
vol.Required(
CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL
): selector.BooleanSelector(),
}
)

CONFIG_FLOW = {
"user": SchemaFlowFormStep(
schema=CONFIG_SCHEMA, validate_user_input=validate_user_input
),
}


class WebminConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for Webmin."""

config_flow = CONFIG_FLOW

def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
return str(options[CONF_HOST])
10 changes: 10 additions & 0 deletions homeassistant/components/webmin/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Constants for the Webmin integration."""

from logging import Logger, getLogger

LOGGER: Logger = getLogger(__package__)
DOMAIN = "webmin"

DEFAULT_PORT = 10000
DEFAULT_SSL = True
DEFAULT_VERIFY_SSL = False
53 changes: 53 additions & 0 deletions homeassistant/components/webmin/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Data update coordinator for the Webmin integration."""
from __future__ import annotations

from typing import Any

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS, CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import DOMAIN, LOGGER
from .helpers import get_instance_from_options, get_sorted_mac_addresses


class WebminUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""The Webmin data update coordinator."""

mac_address: str

def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the Webmin data update coordinator."""

super().__init__(
hass, logger=LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL
)

self.instance, base_url = get_instance_from_options(hass, config_entry.options)

self.device_info = DeviceInfo(
configuration_url=base_url,
name=config_entry.options[CONF_HOST],
)
autinerd marked this conversation as resolved.
Show resolved Hide resolved

async def async_setup(self) -> None:
"""Provide needed data to the device info."""
mac_addresses = get_sorted_mac_addresses(self.data)
self.mac_address = mac_addresses[0]
self.device_info[ATTR_CONNECTIONS] = {
(CONNECTION_NETWORK_MAC, format_mac(mac_address))
for mac_address in mac_addresses
}
self.device_info[ATTR_IDENTIFIERS] = {
(DOMAIN, format_mac(mac_address)) for mac_address in mac_addresses
}

async def _async_update_data(self) -> dict[str, Any]:
return await self.instance.update()
47 changes: 47 additions & 0 deletions homeassistant/components/webmin/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Helper functions for the Webmin integration."""

from collections.abc import Mapping
from typing import Any

from webmin_xmlrpc.client import WebminInstance
from yarl import URL

from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_create_clientsession


def get_instance_from_options(
hass: HomeAssistant, options: Mapping[str, Any]
) -> tuple[WebminInstance, URL]:
"""Retrieve a Webmin instance and the base URL from config options."""

base_url = URL.build(
scheme="https" if options[CONF_SSL] else "http",
user=options[CONF_USERNAME],
password=options[CONF_PASSWORD],
host=options[CONF_HOST],
port=int(options[CONF_PORT]),
)

return WebminInstance(
session=async_create_clientsession(
hass,
verify_ssl=options[CONF_VERIFY_SSL],
base_url=base_url,
)
), base_url


def get_sorted_mac_addresses(data: dict[str, Any]) -> list[str]:
"""Return a sorted list of mac addresses."""
return sorted(
[iface["ether"] for iface in data["active_interfaces"] if "ether" in iface]
)
27 changes: 27 additions & 0 deletions homeassistant/components/webmin/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"entity": {
"sensor": {
"load_1m": {
"default": "mdi:chip"
},
"load_5m": {
"default": "mdi:chip"
},
"load_15m": {
"default": "mdi:chip"
},
"mem_total": {
"default": "mdi:memory"
},
"mem_free": {
"default": "mdi:memory"
},
"swap_total": {
"default": "mdi:memory"
},
"swap_free": {
"default": "mdi:memory"
}
}
}
}
11 changes: 11 additions & 0 deletions homeassistant/components/webmin/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "webmin",
"name": "Webmin",
"codeowners": ["@autinerd"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/webmin",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["webmin"],
"requirements": ["webmin-xmlrpc==0.0.1"]
}
Loading
Loading