Skip to content
Open
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 .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ homeassistant.components.bang_olufsen.*
homeassistant.components.bayesian.*
homeassistant.components.besen.*
homeassistant.components.binary_sensor.*
homeassistant.components.birdnet_go.*
homeassistant.components.bitcoin.*
homeassistant.components.blockchain.*
homeassistant.components.blue_current.*
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.

33 changes: 33 additions & 0 deletions homeassistant/components/birdnet_go/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""The BirdNET-Go integration."""

from aiobirdnetgo import BirdNetGoClient

from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .coordinator import BirdNetGoConfigEntry, BirdNetGoDataUpdateCoordinator

PLATFORMS: list[Platform] = [Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: BirdNetGoConfigEntry) -> bool:
"""Set up BirdNET-Go from a config entry."""
client = BirdNetGoClient(
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
use_ssl=entry.data[CONF_SSL],
session=async_get_clientsession(hass),
)

coordinator = BirdNetGoDataUpdateCoordinator(hass, entry, client)
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: BirdNetGoConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
103 changes: 103 additions & 0 deletions homeassistant/components/birdnet_go/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Config flow for BirdNET-Go integration."""

from typing import Any, override

from aiobirdnetgo import (
BirdNetGoAuthenticationError,
BirdNetGoClient,
BirdNetGoConnectionError,
BirdNetGoError,
BirdNetGoTimeoutError,
)
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
BooleanSelector,
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
TextSelector,
)

from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, LOGGER

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): TextSelector(),
vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.All(
NumberSelector(
NumberSelectorConfig(
min=1,
max=65535,
step=1,
mode=NumberSelectorMode.BOX,
)
),
vol.Coerce(int),
),
vol.Optional(CONF_SSL, default=False): BooleanSelector(),
}
)


class BirdNetGoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for BirdNET-Go."""

VERSION = 1

@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:
raw_host = user_input[CONF_HOST].strip()
raw_port = int(user_input[CONF_PORT])
raw_ssl = bool(user_input[CONF_SSL])

session = async_get_clientsession(self.hass)
try:
client = BirdNetGoClient(
host=raw_host,
port=raw_port,
use_ssl=raw_ssl,
session=session,
)
await client.get_kpis()
Comment thread
TN-1 marked this conversation as resolved.
except ValueError:
errors["base"] = "cannot_connect"
except BirdNetGoAuthenticationError:
errors["base"] = "auth_not_supported"
except BirdNetGoConnectionError, BirdNetGoTimeoutError:
errors["base"] = "cannot_connect"
except BirdNetGoError:
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception during BirdNET-Go setup")
errors["base"] = "unknown"
else:
if not errors:
host = client.host
port = client.port
use_ssl = client.use_ssl
user_input[CONF_HOST] = host
user_input[CONF_PORT] = port
user_input[CONF_SSL] = use_ssl

unique_id = f"{host}:{port}"
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()

title = f"{DEFAULT_NAME} ({host}:{port})"
Comment thread
TN-1 marked this conversation as resolved.
return self.async_create_entry(title=title, data=user_input)

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

from datetime import timedelta
import logging
from typing import Final

DOMAIN: Final = "birdnet_go"
LOGGER = logging.getLogger(__package__)

DEFAULT_NAME: Final = "BirdNET-Go"
DEFAULT_PORT: Final = 8080
SCAN_INTERVAL: Final = timedelta(seconds=30)
61 changes: 61 additions & 0 deletions homeassistant/components/birdnet_go/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""DataUpdateCoordinator for BirdNET-Go."""

from typing import override

from aiobirdnetgo import (
BirdNetGoAuthenticationError,
BirdNetGoClient,
BirdNetGoConnectionError,
BirdNetGoError,
BirdNetGoTimeoutError,
DashboardKPIs,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, LOGGER, SCAN_INTERVAL

type BirdNetGoConfigEntry = ConfigEntry[BirdNetGoDataUpdateCoordinator]


class BirdNetGoDataUpdateCoordinator(DataUpdateCoordinator[DashboardKPIs]):
"""Class to manage fetching BirdNET-Go data."""

config_entry: BirdNetGoConfigEntry

def __init__(
self,
hass: HomeAssistant,
config_entry: BirdNetGoConfigEntry,
client: BirdNetGoClient,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
LOGGER,
config_entry=config_entry,
name=f"{DOMAIN}_{config_entry.title}",
update_interval=SCAN_INTERVAL,
)
self.client = client

@override
async def _async_update_data(self) -> DashboardKPIs:
"""Fetch data from BirdNET-Go."""
try:
return await self.client.get_kpis()
except BirdNetGoAuthenticationError as err:
raise ConfigEntryAuthFailed(
f"Authentication failed for {self.client.base_url}"
) from err
except (BirdNetGoConnectionError, BirdNetGoTimeoutError) as err:
raise UpdateFailed(
f"Error communicating with BirdNET-Go at {self.client.base_url}: {err}"
) from err
except BirdNetGoError as err:
raise UpdateFailed(
f"Unexpected error communicating with BirdNET-Go: {err}"
) from err
18 changes: 18 additions & 0 deletions homeassistant/components/birdnet_go/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"entity": {
"sensor": {
"best_day_count": {
"default": "mdi:trophy-award"
},
"detection_streak": {
"default": "mdi:calendar-range"
},
"lifetime_species": {
"default": "mdi:counter"
},
"today_detections": {
"default": "mdi:bird"
}
}
}
}
11 changes: 11 additions & 0 deletions homeassistant/components/birdnet_go/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "birdnet_go",
"name": "BirdNET-Go",
"codeowners": ["@TN-1"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/birdnet_go",
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["aiobirdnetgo==0.1.5"]
}
86 changes: 86 additions & 0 deletions homeassistant/components/birdnet_go/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
rules:
# Bronze
action-setup:
status: exempt
comment: There are no custom 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: There are no custom 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:
status: exempt
comment: Entities do not explicitly subscribe to 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: There are no custom actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: There are no options flow parameters.
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: There is no discovery.
discovery:
status: exempt
comment: There is no discovery.
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: Station device is created at config entry setup and does not change at runtime.
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: There are no repairable issues.
stale-devices:
status: exempt
comment: Station device is created at config entry setup and does not change at runtime.

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