mirror of
https://github.com/home-assistant/core.git
synced 2026-08-27 18:14:46 -05:00
Add LibreNMS integration (#176825)
This commit is contained in:
@@ -350,6 +350,7 @@ homeassistant.components.letpot.*
|
||||
homeassistant.components.lg_infrared.*
|
||||
homeassistant.components.lg_tv_rs232.*
|
||||
homeassistant.components.libre_hardware_monitor.*
|
||||
homeassistant.components.librenms.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.liebherr.*
|
||||
homeassistant.components.lifx.*
|
||||
|
||||
Generated
+2
@@ -1038,6 +1038,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/lg_tv_rs232/ @balloob
|
||||
/homeassistant/components/libre_hardware_monitor/ @Sab44
|
||||
/tests/components/libre_hardware_monitor/ @Sab44
|
||||
/homeassistant/components/librenms/ @mib1185
|
||||
/tests/components/librenms/ @mib1185
|
||||
/homeassistant/components/lichess/ @aryanhasgithub
|
||||
/tests/components/lichess/ @aryanhasgithub
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""The LibreNMS integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import LibrenmsConfigEntry, LibrenmsDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LibrenmsConfigEntry) -> bool:
|
||||
"""Set up LibreNMS from a config entry."""
|
||||
|
||||
coordinator = LibrenmsDataUpdateCoordinator(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: LibrenmsConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Binary sensor platform for the LibreNMS integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from aiolibrenms.devices.models import LibrenmsDeviceInfo
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import LibrenmsConfigEntry, LibrenmsDataUpdateCoordinator
|
||||
from .entity import LibrenmsDeviceEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LibrenmsDeviceBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Librenms device sensor entity description."""
|
||||
|
||||
value: Callable[[LibrenmsDeviceInfo], bool]
|
||||
is_suitable: Callable[[LibrenmsDeviceInfo], bool] = lambda _: True
|
||||
|
||||
|
||||
DEVICE_SENSOR_TYPES: tuple[LibrenmsDeviceBinarySensorEntityDescription, ...] = (
|
||||
LibrenmsDeviceBinarySensorEntityDescription(
|
||||
key="status",
|
||||
translation_key="status",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
value=lambda data: data.status,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LibrenmsConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add LibreNMS server state sensors."""
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
LibrenmsDeviceBinarySensorEntity(coordinator, description, dev_id)
|
||||
for description in DEVICE_SENSOR_TYPES
|
||||
for dev_id, dev in coordinator.data.devices.items()
|
||||
if description.is_suitable(dev)
|
||||
)
|
||||
|
||||
|
||||
class LibrenmsDeviceBinarySensorEntity(LibrenmsDeviceEntity, BinarySensorEntity):
|
||||
"""Define Librenms sensor entity."""
|
||||
|
||||
entity_description: LibrenmsDeviceBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LibrenmsDataUpdateCoordinator,
|
||||
description: LibrenmsDeviceBinarySensorEntityDescription,
|
||||
device_id: int,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.entry_id}_{device_id}_{description.key}"
|
||||
)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
"""Return the value reported by the sensor."""
|
||||
return self.entity_description.value(self._data)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Config flow for the LibreNMS integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from aiolibrenms import Librenms
|
||||
from aiolibrenms.const import CONNECT_ERRORS
|
||||
from aiolibrenms.exceptions import LibrenmsUnauthenticatedError
|
||||
import voluptuous as vol
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_URL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import DEFAULT_VERIFY_SSL, DOMAIN
|
||||
|
||||
|
||||
class InvalidUrl(HomeAssistantError):
|
||||
"""Error to indicate invalid URL."""
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_URL): TextSelector(
|
||||
config=TextSelectorConfig(type=TextSelectorType.URL)
|
||||
),
|
||||
vol.Required(CONF_API_KEY): TextSelector(
|
||||
config=TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _parse_url(url: str) -> tuple[str, int, bool]:
|
||||
"""Parse the URL and return host, port, and ssl."""
|
||||
parsed_url = URL(url)
|
||||
if (
|
||||
(host := parsed_url.host) is None
|
||||
or (port := parsed_url.port) is None
|
||||
or (scheme := parsed_url.scheme) is None
|
||||
):
|
||||
raise InvalidUrl
|
||||
return host, port, scheme == "https"
|
||||
|
||||
|
||||
async def check_connection(
|
||||
hass: HomeAssistant, host: str, port: int, ssl: bool, verify_ssl: bool, api_key: str
|
||||
) -> None:
|
||||
"""Test connection."""
|
||||
session = async_get_clientsession(hass, verify_ssl)
|
||||
lnms = Librenms(session, api_key, host, port, ssl)
|
||||
await lnms.system.async_get_system_info()
|
||||
|
||||
|
||||
class LibrenmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for LibreNMS."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_name: str
|
||||
_current_data: Mapping[str, Any]
|
||||
|
||||
@override
|
||||
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:
|
||||
(host, port, ssl) = _parse_url(user_input[CONF_URL])
|
||||
except InvalidUrl:
|
||||
errors[CONF_URL] = "invalid_url"
|
||||
else:
|
||||
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
|
||||
try:
|
||||
await check_connection(
|
||||
self.hass,
|
||||
host,
|
||||
port,
|
||||
ssl,
|
||||
user_input[CONF_VERIFY_SSL],
|
||||
user_input[CONF_API_KEY],
|
||||
)
|
||||
except LibrenmsUnauthenticatedError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except CONNECT_ERRORS:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=host,
|
||||
data={
|
||||
CONF_HOST: host,
|
||||
CONF_PORT: port,
|
||||
CONF_SSL: ssl,
|
||||
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Constants for the LibreNMS integration."""
|
||||
|
||||
DOMAIN = "librenms"
|
||||
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Coordinator for the LibreNMS integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from aiolibrenms import Librenms
|
||||
from aiolibrenms.const import CONNECT_ERRORS
|
||||
from aiolibrenms.devices.models import LibrenmsDeviceInfo
|
||||
from aiolibrenms.exceptions import LibrenmsUnauthenticatedError
|
||||
from aiolibrenms.system.models import LibrenmsSystemInfo
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LibrenmsData:
|
||||
"""Data class for storing data from the API."""
|
||||
|
||||
system: LibrenmsSystemInfo
|
||||
devices: dict[int, LibrenmsDeviceInfo]
|
||||
|
||||
|
||||
type LibrenmsConfigEntry = ConfigEntry[LibrenmsDataUpdateCoordinator]
|
||||
|
||||
|
||||
class LibrenmsDataUpdateCoordinator(DataUpdateCoordinator[LibrenmsData]):
|
||||
"""Class to manage fetching LibreNMS data."""
|
||||
|
||||
config_entry: LibrenmsConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: LibrenmsConfigEntry) -> None:
|
||||
"""Initialize the data update coordinator."""
|
||||
self.api = Librenms(
|
||||
async_get_clientsession(hass, config_entry.data[CONF_VERIFY_SSL]),
|
||||
config_entry.data[CONF_API_KEY],
|
||||
config_entry.data[CONF_HOST],
|
||||
config_entry.data[CONF_PORT],
|
||||
config_entry.data[CONF_SSL],
|
||||
)
|
||||
self.configuration_url = str(
|
||||
URL.build(
|
||||
scheme="https" if config_entry.data[CONF_SSL] else "http",
|
||||
host=config_entry.data[CONF_HOST],
|
||||
port=config_entry.data[CONF_PORT],
|
||||
)
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=60),
|
||||
)
|
||||
|
||||
@override
|
||||
async def _async_setup(self) -> None:
|
||||
"""Handle setup of the coordinator."""
|
||||
try:
|
||||
await self.api.system.async_get_system_info()
|
||||
except LibrenmsUnauthenticatedError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
) from err
|
||||
except CONNECT_ERRORS as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> LibrenmsData:
|
||||
"""Update data via internal method."""
|
||||
try:
|
||||
system = await self.api.system.async_get_system_info()
|
||||
devices = await self.api.devices.async_get_devices()
|
||||
except LibrenmsUnauthenticatedError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
) from err
|
||||
except CONNECT_ERRORS as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
return LibrenmsData(system, {dev.device_id: dev for dev in devices})
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Base entity for the LibreNMS integration."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from aiolibrenms.devices.models import LibrenmsDeviceInfo
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LibrenmsDataUpdateCoordinator
|
||||
|
||||
|
||||
class LibrenmsDeviceEntity(CoordinatorEntity[LibrenmsDataUpdateCoordinator]):
|
||||
"""Define LibreNMS device base entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LibrenmsDataUpdateCoordinator,
|
||||
device_id: int,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self.device_id = device_id
|
||||
|
||||
identifier = f"{coordinator.config_entry.entry_id}_{self.device_id}"
|
||||
sw_version = self._data.version
|
||||
model = None
|
||||
configuration_url = f"{coordinator.configuration_url}/device/{self.device_id}"
|
||||
if self._data.os != "ping":
|
||||
if sw_version and (feature := self._data.features) is not None:
|
||||
sw_version += f" ({feature})"
|
||||
model = self._data.hardware
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, identifier)},
|
||||
sw_version=sw_version,
|
||||
configuration_url=configuration_url,
|
||||
name=self._data.display,
|
||||
model=model,
|
||||
serial_number=self._data.serial,
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.device_id in self.coordinator.data.devices
|
||||
|
||||
@property
|
||||
def _data(self) -> LibrenmsDeviceInfo:
|
||||
"""Get DeviceInfo from coordinator."""
|
||||
return self.coordinator.data.devices[self.device_id]
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "librenms",
|
||||
"name": "LibreNMS",
|
||||
"codeowners": ["@mib1185"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/librenms",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiolibrenms"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiolibrenms==0.0.3"]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: This 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: This integration does not provide actions.
|
||||
docs-conditions:
|
||||
status: exempt
|
||||
comment: This integration does not have any conditions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
docs-triggers:
|
||||
status: exempt
|
||||
comment: This integration does not have any triggers.
|
||||
entity-event-setup: done
|
||||
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: This 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: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Service can't be discovered
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: Service can't be discovered
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No repair issues needed
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"common": {
|
||||
"data_desc_api_key": "API key to connect to your LibreNMS instance.",
|
||||
"data_desc_ssl_verify": "Whether to verify the SSL certificate when SSL encryption is used to connect to your LibreNMS instance.",
|
||||
"data_desc_url": "The full URL of your LibreNMS instance."
|
||||
},
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This LibreNMS instance is already configured."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_url": "The provided URL is invalid.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::librenms::common::data_desc_api_key%]",
|
||||
"url": "[%key:component::librenms::common::data_desc_url%]",
|
||||
"verify_ssl": "[%key:component::librenms::common::data_desc_ssl_verify%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"status": {
|
||||
"name": "Status"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"auth_error": {
|
||||
"message": "Authentication failed, please update your API key"
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to your LibreNMS instance."
|
||||
},
|
||||
"update_error": {
|
||||
"message": "An error occurred while retrieving data from your LibreNMS instance: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -432,6 +432,7 @@ FLOWS = {
|
||||
"lg_thinq",
|
||||
"lg_tv_rs232",
|
||||
"libre_hardware_monitor",
|
||||
"librenms",
|
||||
"lichess",
|
||||
"lidarr",
|
||||
"liebherr",
|
||||
|
||||
@@ -3916,6 +3916,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"librenms": {
|
||||
"name": "LibreNMS",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"lichess": {
|
||||
"name": "Lichess",
|
||||
"integration_type": "service",
|
||||
|
||||
@@ -3258,6 +3258,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.librenms.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.lidarr.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
Generated
+3
@@ -323,6 +323,9 @@ aiokef==0.2.16
|
||||
# homeassistant.components.rehlko
|
||||
aiokem==1.0.1
|
||||
|
||||
# homeassistant.components.librenms
|
||||
aiolibrenms==0.0.3
|
||||
|
||||
# homeassistant.components.lichess
|
||||
aiolichess==1.3.0
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Tests for the LibreNMS integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Common fixtures for the LibreNMS tests."""
|
||||
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiolibrenms.devices import LibrenmsDevices
|
||||
from aiolibrenms.system import LibrenmsSystem
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.librenms.const import DOMAIN
|
||||
|
||||
from .const import MOCK_CONFIG_ENTRY_DATA, MOCK_DEVICES_DATA, MOCK_SYSTEM_DATA
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.librenms.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_CONFIG_ENTRY_DATA,
|
||||
title="librenms",
|
||||
entry_id="01KXX1E2EMMSCDQ2K4A0C7JA9T",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_librenms_system() -> AsyncMock:
|
||||
"""Mock the LibreNMS system api."""
|
||||
mock = AsyncMock(spec=LibrenmsSystem)
|
||||
mock.async_get_system_info.return_value = MOCK_SYSTEM_DATA
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_librenms_devices() -> AsyncMock:
|
||||
"""Mock the LibreNMS devices api."""
|
||||
mock = AsyncMock(spec=LibrenmsDevices)
|
||||
mock.async_get_devices.return_value = MOCK_DEVICES_DATA
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_librenms(
|
||||
mock_librenms_devices: AsyncMock,
|
||||
mock_librenms_system: AsyncMock,
|
||||
) -> AsyncGenerator[AsyncMock]:
|
||||
"""Mock the LibreNMS API."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.librenms.coordinator.Librenms", autospec=True
|
||||
) as mock_librenms,
|
||||
patch(
|
||||
"homeassistant.components.librenms.config_flow.Librenms", new=mock_librenms
|
||||
),
|
||||
):
|
||||
client = mock_librenms.return_value
|
||||
client.devices = mock_librenms_devices
|
||||
client.system = mock_librenms_system
|
||||
yield client
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Constants for the LibreNMS integration tests."""
|
||||
|
||||
from aiolibrenms.devices.models import LibrenmsDeviceInfo
|
||||
from aiolibrenms.system.models import LibrenmsSystemInfo
|
||||
|
||||
from homeassistant.components.librenms.const import DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_URL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
MOCK_USER_DATA = {
|
||||
CONF_URL: "https://librenms",
|
||||
CONF_API_KEY: "abcdef0123456789",
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
|
||||
MOCK_CONFIG_ENTRY_DATA = {
|
||||
CONF_HOST: "librenms",
|
||||
CONF_API_KEY: "abcdef0123456789",
|
||||
CONF_PORT: 443,
|
||||
CONF_SSL: True,
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
|
||||
MOCK_SYSTEM_DATA = LibrenmsSystemInfo.from_json(
|
||||
load_fixture("system_data.json", DOMAIN)
|
||||
)
|
||||
|
||||
MOCK_DEVICES_DATA = [
|
||||
LibrenmsDeviceInfo.from_json(load_fixture("device_1.json", DOMAIN)),
|
||||
LibrenmsDeviceInfo.from_json(load_fixture("device_3.json", DOMAIN)),
|
||||
LibrenmsDeviceInfo.from_json(load_fixture("device_13.json", DOMAIN)),
|
||||
LibrenmsDeviceInfo.from_json(load_fixture("device_29.json", DOMAIN)),
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"device_id": 1,
|
||||
"inserted": "2023-09-16 11:29:40",
|
||||
"hostname": "192.168.100.1",
|
||||
"sysName": "sophosxg",
|
||||
"display": "SophosXG",
|
||||
"display_template": "SophosXG",
|
||||
"ip": "192.168.100.1",
|
||||
"overwrite_ip": "",
|
||||
"community": "read-only",
|
||||
"authlevel": null,
|
||||
"authname": null,
|
||||
"authpass": null,
|
||||
"authalgo": null,
|
||||
"cryptopass": null,
|
||||
"cryptoalgo": null,
|
||||
"snmpver": "v2c",
|
||||
"port": 161,
|
||||
"transport": "udp",
|
||||
"timeout": null,
|
||||
"retries": null,
|
||||
"snmp_disable": 0,
|
||||
"bgpLocalAs": null,
|
||||
"sysObjectID": ".1.3.6.1.4.1.2604.5",
|
||||
"snmpEngineID": "80 00 1F 88 80 BD 17 B9 64 41 DA 99 64",
|
||||
"sysDescr": "Linux localhost 6.6.116 #1 SMP Fri Feb 27 22:12:13 CST 2026 x86_64",
|
||||
"sysContact": "me",
|
||||
"version": "22.0.1 MR-1-Build490",
|
||||
"hardware": "SFVH_KV01_SFOS",
|
||||
"features": null,
|
||||
"location_id": 1,
|
||||
"os": "sophos-xg",
|
||||
"status": 1,
|
||||
"status_reason": "",
|
||||
"ignore": 0,
|
||||
"disabled": 0,
|
||||
"uptime": 2592289,
|
||||
"agent_uptime": 0,
|
||||
"last_polled": "2026-07-13 22:19:47",
|
||||
"last_poll_attempted": null,
|
||||
"last_polled_timetaken": 1.6974771022797,
|
||||
"last_discovered_timetaken": 2.6829509735107,
|
||||
"last_discovered": "2026-07-13 20:00:04",
|
||||
"last_ping": "2026-07-13 22:20:03",
|
||||
"last_ping_timetaken": 0.149,
|
||||
"purpose": "",
|
||||
"type": "appliance",
|
||||
"serial": "C01yyyyyyxxxxxx",
|
||||
"icon": "sophos-xg.png",
|
||||
"poller_group": 0,
|
||||
"override_sysLocation": 0,
|
||||
"notes": null,
|
||||
"port_association_mode": 1,
|
||||
"max_depth": 2,
|
||||
"disable_notify": 0,
|
||||
"ignore_status": 0,
|
||||
"mtu_status": 1,
|
||||
"dependency_parent_id": "5",
|
||||
"dependency_parent_hostname": "192.168.100.2",
|
||||
"location": "Home",
|
||||
"lat": null,
|
||||
"lng": null
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"device_id": 13,
|
||||
"inserted": "2023-09-23 19:45:29",
|
||||
"hostname": "192.168.1.5",
|
||||
"sysName": "homeassistant",
|
||||
"display": "homeassistant",
|
||||
"display_template": null,
|
||||
"ip": "192.168.1.5",
|
||||
"overwrite_ip": "",
|
||||
"community": "read-only",
|
||||
"authlevel": null,
|
||||
"authname": null,
|
||||
"authpass": null,
|
||||
"authalgo": null,
|
||||
"cryptopass": null,
|
||||
"cryptoalgo": null,
|
||||
"snmpver": "v2c",
|
||||
"port": 161,
|
||||
"transport": "udp",
|
||||
"timeout": null,
|
||||
"retries": null,
|
||||
"snmp_disable": 0,
|
||||
"bgpLocalAs": null,
|
||||
"sysObjectID": ".1.3.6.1.4.1.8072.3.2.10",
|
||||
"snmpEngineID": "80 00 1F 88 80 E7 73 4D 32 ED 2A 54 6A 00 00 00 \n00",
|
||||
"sysDescr": "Linux homeassistant 6.18.37-haos #1 SMP PREEMPT_DYNAMIC Wed Jul 1 07:35:33 UTC 2026 x86_64 (Home Assistant OS 18.1)",
|
||||
"sysContact": "me",
|
||||
"version": "6.18.37-haos",
|
||||
"hardware": "QEMU Standard PC (i440FX + PIIX, 1996)",
|
||||
"features": "Home Assistant OS 18.1",
|
||||
"location_id": 4,
|
||||
"os": "linux",
|
||||
"status": 1,
|
||||
"status_reason": "",
|
||||
"ignore": 0,
|
||||
"disabled": 0,
|
||||
"uptime": 72931,
|
||||
"agent_uptime": 0,
|
||||
"last_polled": "2026-07-13 22:17:06",
|
||||
"last_poll_attempted": null,
|
||||
"last_polled_timetaken": 1.6433110237122,
|
||||
"last_discovered_timetaken": 2.8492691516876,
|
||||
"last_discovered": "2026-07-13 20:00:10",
|
||||
"last_ping": "2026-07-13 22:20:03",
|
||||
"last_ping_timetaken": 0.772,
|
||||
"purpose": "",
|
||||
"type": "server",
|
||||
"serial": null,
|
||||
"icon": "linux.svg",
|
||||
"poller_group": 0,
|
||||
"override_sysLocation": 1,
|
||||
"notes": null,
|
||||
"port_association_mode": 1,
|
||||
"max_depth": 2,
|
||||
"disable_notify": 0,
|
||||
"ignore_status": 0,
|
||||
"mtu_status": 1,
|
||||
"dependency_parent_id": "5",
|
||||
"dependency_parent_hostname": "192.168.100.2",
|
||||
"location": "Home",
|
||||
"lat": null,
|
||||
"lng": null
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"device_id": 29,
|
||||
"inserted": "2024-11-13 10:47:15",
|
||||
"hostname": "104.21.87.21",
|
||||
"sysName": "firmware.esphome.io (104.21.87.21)",
|
||||
"display": "firmware.esphome.io (104.21.87.21)",
|
||||
"display_template": null,
|
||||
"ip": "104.21.87.21",
|
||||
"overwrite_ip": null,
|
||||
"community": null,
|
||||
"authlevel": null,
|
||||
"authname": null,
|
||||
"authpass": null,
|
||||
"authalgo": null,
|
||||
"cryptopass": null,
|
||||
"cryptoalgo": null,
|
||||
"snmpver": "v2c",
|
||||
"port": 161,
|
||||
"transport": "udp",
|
||||
"timeout": null,
|
||||
"retries": null,
|
||||
"snmp_disable": 1,
|
||||
"bgpLocalAs": null,
|
||||
"sysObjectID": null,
|
||||
"snmpEngineID": null,
|
||||
"sysDescr": null,
|
||||
"sysContact": null,
|
||||
"version": null,
|
||||
"hardware": "",
|
||||
"features": null,
|
||||
"location_id": null,
|
||||
"os": "ping",
|
||||
"status": 1,
|
||||
"status_reason": "",
|
||||
"ignore": 0,
|
||||
"disabled": 0,
|
||||
"uptime": null,
|
||||
"agent_uptime": 0,
|
||||
"last_polled": "2026-07-13 22:18:41",
|
||||
"last_poll_attempted": null,
|
||||
"last_polled_timetaken": 1.1394469738007,
|
||||
"last_discovered_timetaken": 1.0503590106964,
|
||||
"last_discovered": "2026-07-13 20:00:29",
|
||||
"last_ping": "2026-07-13 22:20:03",
|
||||
"last_ping_timetaken": 95.6,
|
||||
"purpose": null,
|
||||
"type": "",
|
||||
"serial": null,
|
||||
"icon": null,
|
||||
"poller_group": 0,
|
||||
"override_sysLocation": 0,
|
||||
"notes": null,
|
||||
"port_association_mode": 1,
|
||||
"max_depth": 0,
|
||||
"disable_notify": 0,
|
||||
"ignore_status": 0,
|
||||
"mtu_status": 1,
|
||||
"dependency_parent_id": null,
|
||||
"dependency_parent_hostname": null,
|
||||
"location": null,
|
||||
"lat": null,
|
||||
"lng": null
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"device_id": 3,
|
||||
"inserted": "2023-09-16 11:34:51",
|
||||
"hostname": "192.168.1.104",
|
||||
"sysName": "printer",
|
||||
"display": "Drucker",
|
||||
"display_template": "Drucker",
|
||||
"ip": "192.168.1.104",
|
||||
"overwrite_ip": "",
|
||||
"community": "read-only",
|
||||
"authlevel": null,
|
||||
"authname": null,
|
||||
"authpass": null,
|
||||
"authalgo": null,
|
||||
"cryptopass": null,
|
||||
"cryptoalgo": null,
|
||||
"snmpver": "v2c",
|
||||
"port": 161,
|
||||
"transport": "udp",
|
||||
"timeout": null,
|
||||
"retries": null,
|
||||
"snmp_disable": 0,
|
||||
"bgpLocalAs": null,
|
||||
"sysObjectID": ".1.3.6.1.4.1.641.52.71107331",
|
||||
"snmpEngineID": "",
|
||||
"sysDescr": "Lexmark MC2425adw version CXNZJ.250.038 kernel 6.6.86-yocto-standard All-N-1",
|
||||
"sysContact": "me",
|
||||
"version": "CXNZJ.250.038",
|
||||
"hardware": "MC2425adw",
|
||||
"features": null,
|
||||
"location_id": 3,
|
||||
"os": "lexmarkprinter",
|
||||
"status": 1,
|
||||
"status_reason": "",
|
||||
"ignore": 0,
|
||||
"disabled": 0,
|
||||
"uptime": 15830868,
|
||||
"agent_uptime": 0,
|
||||
"last_polled": "2026-07-13 22:17:46",
|
||||
"last_poll_attempted": null,
|
||||
"last_polled_timetaken": 1.7405550479889,
|
||||
"last_discovered_timetaken": 2.7769849300385,
|
||||
"last_discovered": "2026-07-13 20:00:07",
|
||||
"last_ping": "2026-07-13 22:20:03",
|
||||
"last_ping_timetaken": 0.812,
|
||||
"purpose": "",
|
||||
"type": "printer",
|
||||
"serial": "7529936145YFW",
|
||||
"icon": "lexmark.svg",
|
||||
"poller_group": 0,
|
||||
"override_sysLocation": 0,
|
||||
"notes": null,
|
||||
"port_association_mode": 1,
|
||||
"max_depth": 0,
|
||||
"disable_notify": 1,
|
||||
"ignore_status": 0,
|
||||
"mtu_status": 1,
|
||||
"dependency_parent_id": null,
|
||||
"dependency_parent_hostname": null,
|
||||
"location": "Home",
|
||||
"lat": null,
|
||||
"lng": null
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"local_ver": "26.6.1",
|
||||
"local_sha": "",
|
||||
"local_date": "2026-06-18T09:10:36+02:00",
|
||||
"local_branch": "",
|
||||
"db_schema": "2026_06_09_000000_change_vsz_to_big_int_processes_table (390)",
|
||||
"php_ver": "8.4.21",
|
||||
"python_ver": "3.12.13",
|
||||
"database_ver": "MariaDB 10.5.29-MariaDB-ubu2004",
|
||||
"rrdtool_ver": "1.9.0",
|
||||
"netsnmp_ver": "5.9.5.2"
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensors.8
|
||||
list([
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'configuration_url': 'https://librenms/device/1',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'librenms',
|
||||
'01KXX1E2EMMSCDQ2K4A0C7JA9T_1',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': 'SFVH_KV01_SFOS',
|
||||
'model_id': None,
|
||||
'name': 'SophosXG',
|
||||
'name_by_user': None,
|
||||
'serial_number': 'C01yyyyyyxxxxxx',
|
||||
'sw_version': '22.0.1 MR-1-Build490',
|
||||
'via_device_id': None,
|
||||
}),
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'configuration_url': 'https://librenms/device/3',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'librenms',
|
||||
'01KXX1E2EMMSCDQ2K4A0C7JA9T_3',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': 'MC2425adw',
|
||||
'model_id': None,
|
||||
'name': 'Drucker',
|
||||
'name_by_user': None,
|
||||
'serial_number': '7529936145YFW',
|
||||
'sw_version': 'CXNZJ.250.038',
|
||||
'via_device_id': None,
|
||||
}),
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'configuration_url': 'https://librenms/device/13',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'librenms',
|
||||
'01KXX1E2EMMSCDQ2K4A0C7JA9T_13',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': 'QEMU Standard PC (i440FX + PIIX, 1996)',
|
||||
'model_id': None,
|
||||
'name': 'homeassistant',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'sw_version': '6.18.37-haos (Home Assistant OS 18.1)',
|
||||
'via_device_id': None,
|
||||
}),
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'configuration_url': 'https://librenms/device/29',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'librenms',
|
||||
'01KXX1E2EMMSCDQ2K4A0C7JA9T_29',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': None,
|
||||
'model_id': None,
|
||||
'name': 'firmware.esphome.io (104.21.87.21)',
|
||||
'name_by_user': None,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_sensors[binary_sensor.drucker_status-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.drucker_status',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Status',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status',
|
||||
'platform': 'librenms',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'status',
|
||||
'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_3_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[binary_sensor.drucker_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'connectivity',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Drucker Status',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.drucker_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[binary_sensor.firmware_esphome_io_104_21_87_21_status-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.firmware_esphome_io_104_21_87_21_status',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Status',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status',
|
||||
'platform': 'librenms',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'status',
|
||||
'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_29_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[binary_sensor.firmware_esphome_io_104_21_87_21_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'connectivity',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'firmware.esphome.io (104.21.87.21) Status',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.firmware_esphome_io_104_21_87_21_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[binary_sensor.homeassistant_status-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.homeassistant_status',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Status',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status',
|
||||
'platform': 'librenms',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'status',
|
||||
'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_13_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[binary_sensor.homeassistant_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'connectivity',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'homeassistant Status',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.homeassistant_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[binary_sensor.sophosxg_status-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.sophosxg_status',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Status',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Status',
|
||||
'platform': 'librenms',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'status',
|
||||
'unique_id': '01KXX1E2EMMSCDQ2K4A0C7JA9T_1_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[binary_sensor.sophosxg_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'connectivity',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'SophosXG Status',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.sophosxg_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Test the LibreNMS binary sensor platform."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_librenms: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the LibreNMS binary sensor platform."""
|
||||
|
||||
with patch("homeassistant.components.librenms.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
devices = dr.async_entries_for_config_entry(
|
||||
device_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert devices == snapshot
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Test the LibreNMS config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiolibrenms.exceptions import LibrenmsUnauthenticatedError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.librenms.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .const import MOCK_CONFIG_ENTRY_DATA, MOCK_USER_DATA
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_step_user(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_librenms: Mock
|
||||
) -> None:
|
||||
"""Test a user initiated config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_DATA,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "librenms"
|
||||
assert result["data"] == MOCK_CONFIG_ENTRY_DATA
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(
|
||||
LibrenmsUnauthenticatedError({"message": "Unauthenticated."}),
|
||||
"invalid_auth",
|
||||
),
|
||||
(ClientError, "cannot_connect"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_step_user_error_handling(
|
||||
hass: HomeAssistant, mock_librenms: Mock, exception: Exception, error: str
|
||||
) -> None:
|
||||
"""Test a user initiated config flow with errors."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
mock_librenms.system.async_get_system_info.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_DATA,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_librenms.system.async_get_system_info.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_DATA,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_step_user_invalid_url(hass: HomeAssistant, mock_librenms: Mock) -> None:
|
||||
"""Test a user initiated config flow with errors."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{**MOCK_USER_DATA, CONF_URL: "hts://invalid"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {CONF_URL: "invalid_url"}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_DATA,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_user_already_configured(
|
||||
hass: HomeAssistant, mock_librenms: Mock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test starting a flow by user when already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
MOCK_USER_DATA,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
Reference in New Issue
Block a user