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 17 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
111 changes: 111 additions & 0 deletions homeassistant/components/webmin/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""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 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.helpers import selector
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaConfigFlowHandler,
SchemaFlowError,
SchemaFlowFormStep,
)

from .const import DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN


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
base_url = URL.build(
scheme="https" if user_input[CONF_SSL] else "http",
user=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
host=user_input[CONF_HOST],
port=int(user_input[CONF_PORT]),
)
session = async_create_clientsession(
handler.parent_handler.hass,
verify_ssl=user_input[CONF_VERIFY_SSL],
base_url=base_url,
)
instance = WebminInstance(session=session)
try:
data = await instance.update()
ifaces = [iface for iface in data["active_interfaces"] if "ether" in iface]
ifaces.sort(key=lambda x: x["ether"])
mac_address = ifaces[0]["ether"]
await cast(SchemaConfigFlowHandler, handler.parent_handler).async_set_unique_id(
mac_address
)
return user_input
autinerd marked this conversation as resolved.
Show resolved Hide resolved
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


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
76 changes: 76 additions & 0 deletions homeassistant/components/webmin/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Data update coordinator for the Webmin integration."""
from __future__ import annotations

from typing import Any

from webmin_xmlrpc.client import WebminInstance
from yarl import URL

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_IDENTIFIERS,
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
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


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
)
base_url = URL.build(
scheme="https" if config_entry.options[CONF_SSL] else "http",
user=config_entry.options[CONF_USERNAME],
password=config_entry.options[CONF_PASSWORD],
host=config_entry.options[CONF_HOST],
port=int(config_entry.options[CONF_PORT]),
)
self.instance = WebminInstance(
session=async_create_clientsession(
hass,
verify_ssl=config_entry.options[CONF_VERIFY_SSL],
base_url=base_url,
)
)
autinerd marked this conversation as resolved.
Show resolved Hide resolved
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."""
ifaces = [iface for iface in self.data["active_interfaces"] if "ether" in iface]
ifaces.sort(key=lambda x: x["ether"])
self.mac_address = format_mac(ifaces[0]["ether"])
autinerd marked this conversation as resolved.
Show resolved Hide resolved
self.device_info[ATTR_CONNECTIONS] = {
(CONNECTION_NETWORK_MAC, format_mac(iface["ether"])) for iface in ifaces
}
self.device_info[ATTR_IDENTIFIERS] = {
(DOMAIN, format_mac(iface["ether"])) for iface in ifaces
}

async def _async_update_data(self) -> dict[str, Any]:
return await self.instance.update()
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