Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
5 changes: 4 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"GitHub.vscode-pull-request-github",
"GitHub.copilot"
],
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.jsonc
"settings": {
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
Expand Down Expand Up @@ -63,6 +63,9 @@
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[json][jsonc][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"json.schemas": [
{
"fileMatch": ["homeassistant/components/*/manifest.json"],
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ virtualization/vagrant/config
!.vscode/cSpell.json
!.vscode/extensions.json
!.vscode/tasks.json
!.vscode/settings.default.jsonc
.env

# Windows Explorer
Expand Down
20 changes: 12 additions & 8 deletions .vscode/settings.default.json → .vscode/settings.default.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
"pylint.importStrategy": "fromEnvironment",
// Pyright is too pedantic for Home Assistant
"python.analysis.typeCheckingMode": "basic",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[json][jsonc][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"json.schemas": [
{
"fileMatch": [
"homeassistant/components/*/manifest.json"
],
// This value differs between working with devcontainer and locally, therefor this value should NOT be in sync!
"url": "./script/json_schemas/manifest_schema.json"
}
]
{
"fileMatch": ["homeassistant/components/*/manifest.json"],
// This value differs between working with devcontainer and locally, therefore this value should NOT be in sync!
"url": "./script/json_schemas/manifest_schema.json"
}
]
}
2 changes: 2 additions & 0 deletions CODEOWNERS

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

2 changes: 1 addition & 1 deletion homeassistant/components/airzone/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.1"]
"requirements": ["aioairzone==1.0.2"]
}
42 changes: 42 additions & 0 deletions homeassistant/components/fing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""The Fing integration."""

from __future__ import annotations

import logging

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

from .coordinator import FingConfigEntry, FingDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.DEVICE_TRACKER]


async def async_setup_entry(hass: HomeAssistant, config_entry: FingConfigEntry) -> bool:
"""Set up the Fing component."""

coordinator = FingDataUpdateCoordinator(hass, config_entry)
await coordinator.async_config_entry_first_refresh()

if coordinator.data.network_id is None:
_LOGGER.warning(
"Skip setting up Fing integration; Received an empty NetworkId from the request - Check if the API version is the latest"
)
raise ConfigEntryError(
"The Agent's API version is outdated. Please update the agent to the latest version."
)

config_entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

return True


async def async_unload_entry(
hass: HomeAssistant, config_entry: FingConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
114 changes: 114 additions & 0 deletions homeassistant/components/fing/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Config flow file."""

from contextlib import suppress
import logging
from typing import Any

from fing_agent_api import FingAgent
import httpx
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT

from .const import DOMAIN, UPNP_AVAILABLE

_LOGGER = logging.getLogger(__name__)


class FingConfigFlow(ConfigFlow, domain=DOMAIN):
"""Fing config flow."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Set up user step."""
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}

if user_input is not None:
devices_response = None
agent_info_response = None

self._async_abort_entries_match(
{CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]}
)

fing_api = FingAgent(
ip=user_input[CONF_IP_ADDRESS],
port=int(user_input[CONF_PORT]),
key=user_input[CONF_API_KEY],
)

try:
devices_response = await fing_api.get_devices()

with suppress(httpx.ConnectError):
# The suppression is needed because the get_agent_info method isn't available for desktop agents
agent_info_response = await fing_api.get_agent_info()

except httpx.NetworkError as _:
errors["base"] = "cannot_connect"
except httpx.TimeoutException as _:
errors["base"] = "timeout_connect"
except httpx.HTTPStatusError as exception:
description_placeholders["message"] = (
f"{exception.response.status_code} - {exception.response.reason_phrase}"
)
if exception.response.status_code == 401:
errors["base"] = "invalid_api_key"
else:
errors["base"] = "http_status_error"
except httpx.InvalidURL as _:
errors["base"] = "url_error"
except (
httpx.HTTPError,
httpx.CookieConflict,
httpx.StreamError,
) as ex:
_LOGGER.error("Unexpected exception: %s", ex)
errors["base"] = "unknown"
else:
if (
devices_response.network_id is not None
and len(devices_response.network_id) > 0
):
agent_name = user_input.get(CONF_IP_ADDRESS)
upnp_available = False
if agent_info_response is not None:
upnp_available = True
agent_name = agent_info_response.agent_id
await self.async_set_unique_id(agent_info_response.agent_id)
self._abort_if_unique_id_configured()

data = {
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
CONF_PORT: user_input[CONF_PORT],
CONF_API_KEY: user_input[CONF_API_KEY],
UPNP_AVAILABLE: upnp_available,
}

return self.async_create_entry(
title=f"Fing Agent {agent_name}",
data=data,
)

return self.async_abort(reason="api_version_error")

return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_IP_ADDRESS): str,
vol.Required(CONF_PORT, default="49090"): str,
vol.Required(CONF_API_KEY): str,
}
),
user_input,
),
errors=errors,
description_placeholders=description_placeholders,
)
4 changes: 4 additions & 0 deletions homeassistant/components/fing/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Const for the Fing integration."""

DOMAIN = "fing"
UPNP_AVAILABLE = "upnp_available"
85 changes: 85 additions & 0 deletions homeassistant/components/fing/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""DataUpdateCoordinator for Fing integration."""

from dataclasses import dataclass, field
from datetime import timedelta
import logging

from fing_agent_api import FingAgent
from fing_agent_api.models import AgentInfoResponse, Device
import httpx

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, UPNP_AVAILABLE

_LOGGER = logging.getLogger(__name__)

type FingConfigEntry = ConfigEntry[FingDataUpdateCoordinator]


@dataclass
class FingDataObject:
"""Fing Data Object."""

network_id: str | None = None
agent_info: AgentInfoResponse | None = None
devices: dict[str, Device] = field(default_factory=dict)


class FingDataUpdateCoordinator(DataUpdateCoordinator[FingDataObject]):
"""Class to manage fetching data from Fing Agent."""

def __init__(self, hass: HomeAssistant, config_entry: FingConfigEntry) -> None:
"""Initialize global Fing updater."""
self._fing = FingAgent(
ip=config_entry.data[CONF_IP_ADDRESS],
port=int(config_entry.data[CONF_PORT]),
key=config_entry.data[CONF_API_KEY],
)
self._upnp_available = config_entry.data[UPNP_AVAILABLE]
update_interval = timedelta(seconds=30)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=update_interval,
config_entry=config_entry,
)

async def _async_update_data(self) -> FingDataObject:
"""Fetch data from Fing Agent."""
device_response = None
agent_info_response = None
try:
device_response = await self._fing.get_devices()

if self._upnp_available:
agent_info_response = await self._fing.get_agent_info()

except httpx.NetworkError as err:
raise UpdateFailed("Failed to connect") from err
except httpx.TimeoutException as err:
raise UpdateFailed("Timeout establishing connection") from err
except httpx.HTTPStatusError as err:
if err.response.status_code == 401:
raise UpdateFailed("Invalid API key") from err
raise UpdateFailed(
f"Http request failed -> {err.response.status_code} - {err.response.reason_phrase}"
) from err
except httpx.InvalidURL as err:
raise UpdateFailed("Invalid hostname or IP address") from err
except (
httpx.HTTPError,
httpx.CookieConflict,
httpx.StreamError,
) as err:
raise UpdateFailed("Unexpected error from HTTP request") from err
else:
return FingDataObject(
device_response.network_id,
agent_info_response,
{device.mac: device for device in device_response.devices},
)
Loading
Loading