diff --git a/.devcontainer.json b/.devcontainer/devcontainer.json similarity index 61% rename from .devcontainer.json rename to .devcontainer/devcontainer.json index 562abd0..930ba38 100644 --- a/.devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,23 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python { - "name": "ludeeus/integration_blueprint", + "name": "Home Assistant Dev Container", "image": "mcr.microsoft.com/devcontainers/python:3.12", - "postCreateCommand": "scripts/setup", + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers-extra/features/apt-packages:1": { + "packages": [ + "ffmpeg", + "libturbojpeg0", + "libpcap-dev" + ] + }, + "ghcr.io/devcontainers-extra/features/ruff:1": {}, + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/python:1": {} + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ 8123 ], @@ -11,14 +27,15 @@ "onAutoForward": "notify" } }, + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "scripts/setup", + // Configure tool-specific properties. "customizations": { "vscode": { "extensions": [ "charliermarsh.ruff", "github.vscode-pull-request-github", - "ms-python.python", - "ms-python.vscode-pylance", - "ryanluker.vscode-coverage-gutters" + "ms-python.python" ], "settings": { "files.eol": "\n", @@ -36,14 +53,5 @@ } } }, - "remoteUser": "vscode", - "features": { - "ghcr.io/devcontainers-extra/features/apt-packages:1": { - "packages": [ - "ffmpeg", - "libturbojpeg0", - "libpcap-dev" - ] - } - } + "remoteUser": "vscode" } \ No newline at end of file diff --git a/custom_components/belgiantrain/__init__.py b/custom_components/belgiantrain/__init__.py index c8282ab..431592f 100644 --- a/custom_components/belgiantrain/__init__.py +++ b/custom_components/belgiantrain/__init__.py @@ -2,33 +2,79 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry +from datetime import timedelta +from typing import TYPE_CHECKING + from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.loader import async_get_loaded_integration + +from .api import BelgiantrainApiClient +from .const import DOMAIN, LOGGER +from .coordinator import BelgiantrainDataUpdateCoordinator +from .data import BelgiantrainData + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + + from .data import BelgiantrainConfigEntry # TODO List the platforms that you want to support. # For your initial PR, limit it to 1 platform. -PLATFORMS: list[Platform] = [Platform.LIGHT] - -# TODO Create ConfigEntry type alias with API object -# TODO Rename type alias and update all entry annotations -type New_NameConfigEntry = ConfigEntry[MyApi] # noqa: F821 +PLATFORMS: list[Platform] = [ + Platform.SENSOR, + Platform.BINARY_SENSOR, + Platform.SWITCH, +] # TODO Update entry annotation -async def async_setup_entry(hass: HomeAssistant, entry: New_NameConfigEntry) -> bool: - """Set up SNCB/NMBS from a config entry.""" +# https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry +async def async_setup_entry( + hass: HomeAssistant, + entry: BelgiantrainConfigEntry, +) -> bool: + """Set up the SNCB/NMBS integration from a config entry using UI.""" # TODO 1. Create API instance # TODO 2. Validate the API connection (and authentication) # TODO 3. Store an API object for your platforms to access # entry.runtime_data = MyAPI(...) + coordinator = BelgiantrainDataUpdateCoordinator( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(hours=1), + ) + entry.runtime_data = BelgiantrainData( + client=BelgiantrainApiClient( + session=async_get_clientsession(hass), + ), + integration=async_get_loaded_integration(hass, entry.domain), + coordinator=coordinator, + ) + + # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities + await coordinator.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True # TODO Update entry annotation -async def async_unload_entry(hass: HomeAssistant, entry: New_NameConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, + entry: BelgiantrainConfigEntry, +) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_reload_entry( + hass: HomeAssistant, + entry: BelgiantrainConfigEntry, +) -> None: + """Reload config entry.""" + await async_unload_entry(hass, entry) + await async_setup_entry(hass, entry) diff --git a/custom_components/integration_blueprint/api.py b/custom_components/belgiantrain/api.py similarity index 82% rename from custom_components/integration_blueprint/api.py rename to custom_components/belgiantrain/api.py index 441e745..515a53e 100644 --- a/custom_components/integration_blueprint/api.py +++ b/custom_components/belgiantrain/api.py @@ -9,18 +9,18 @@ import async_timeout -class IntegrationBlueprintApiClientError(Exception): +class BelgiantrainApiClientError(Exception): """Exception to indicate a general API error.""" -class IntegrationBlueprintApiClientCommunicationError( - IntegrationBlueprintApiClientError, +class BelgiantrainApiClientCommunicationError( + BelgiantrainApiClientError, ): """Exception to indicate a communication error.""" -class IntegrationBlueprintApiClientAuthenticationError( - IntegrationBlueprintApiClientError, +class BelgiantrainApiClientAuthenticationError( + BelgiantrainApiClientError, ): """Exception to indicate an authentication error.""" @@ -29,13 +29,13 @@ def _verify_response_or_raise(response: aiohttp.ClientResponse) -> None: """Verify that the response is valid.""" if response.status in (401, 403): msg = "Invalid credentials" - raise IntegrationBlueprintApiClientAuthenticationError( + raise BelgiantrainApiClientAuthenticationError( msg, ) response.raise_for_status() -class IntegrationBlueprintApiClient: +class BelgiantrainApiClient: """Sample API Client.""" def __init__( @@ -86,16 +86,16 @@ async def _api_wrapper( except TimeoutError as exception: msg = f"Timeout error fetching information - {exception}" - raise IntegrationBlueprintApiClientCommunicationError( + raise BelgiantrainApiClientCommunicationError( msg, ) from exception except (aiohttp.ClientError, socket.gaierror) as exception: msg = f"Error fetching information - {exception}" - raise IntegrationBlueprintApiClientCommunicationError( + raise BelgiantrainApiClientCommunicationError( msg, ) from exception except Exception as exception: # pylint: disable=broad-except msg = f"Something really wrong happened! - {exception}" - raise IntegrationBlueprintApiClientError( + raise BelgiantrainApiClientError( msg, ) from exception diff --git a/custom_components/integration_blueprint/binary_sensor.py b/custom_components/belgiantrain/binary_sensor.py similarity index 68% rename from custom_components/integration_blueprint/binary_sensor.py rename to custom_components/belgiantrain/binary_sensor.py index 2491e7e..fa569d4 100644 --- a/custom_components/integration_blueprint/binary_sensor.py +++ b/custom_components/belgiantrain/binary_sensor.py @@ -1,4 +1,4 @@ -"""Binary sensor platform for integration_blueprint.""" +"""Binary sensor platform for belgiantrain.""" from __future__ import annotations @@ -10,19 +10,19 @@ BinarySensorEntityDescription, ) -from .entity import IntegrationBlueprintEntity +from . import BelgiantrainEntity if TYPE_CHECKING: from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback - from .coordinator import BlueprintDataUpdateCoordinator - from .data import IntegrationBlueprintConfigEntry + from . import BelgiantrainDataUpdateCoordinator + from .data import BelgiantrainConfigEntry ENTITY_DESCRIPTIONS = ( BinarySensorEntityDescription( - key="integration_blueprint", - name="Integration Blueprint Binary Sensor", + key="belgiantrain", + name="Belgiantrain Binary Sensor", device_class=BinarySensorDeviceClass.CONNECTIVITY, ), ) @@ -30,12 +30,12 @@ async def async_setup_entry( hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` - entry: IntegrationBlueprintConfigEntry, + entry: BelgiantrainConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the binary_sensor platform.""" async_add_entities( - IntegrationBlueprintBinarySensor( + BelgiantrainBinarySensor( coordinator=entry.runtime_data.coordinator, entity_description=entity_description, ) @@ -43,12 +43,12 @@ async def async_setup_entry( ) -class IntegrationBlueprintBinarySensor(IntegrationBlueprintEntity, BinarySensorEntity): - """integration_blueprint binary_sensor class.""" +class BelgiantrainBinarySensor(BelgiantrainEntity, BinarySensorEntity): + """belgiantrain binary_sensor class.""" def __init__( self, - coordinator: BlueprintDataUpdateCoordinator, + coordinator: BelgiantrainDataUpdateCoordinator, entity_description: BinarySensorEntityDescription, ) -> None: """Initialize the binary_sensor class.""" diff --git a/custom_components/belgiantrain/const.py b/custom_components/belgiantrain/const.py index 8116a15..fc3ab80 100644 --- a/custom_components/belgiantrain/const.py +++ b/custom_components/belgiantrain/const.py @@ -1,3 +1,8 @@ """Constants for the SNCB/NMBS integration.""" +from logging import Logger, getLogger + +LOGGER: Logger = getLogger(__package__) + DOMAIN = "belgiantrain" +ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" diff --git a/custom_components/integration_blueprint/coordinator.py b/custom_components/belgiantrain/coordinator.py similarity index 60% rename from custom_components/integration_blueprint/coordinator.py rename to custom_components/belgiantrain/coordinator.py index 0ba3d69..f831e7e 100644 --- a/custom_components/integration_blueprint/coordinator.py +++ b/custom_components/belgiantrain/coordinator.py @@ -1,4 +1,4 @@ -"""DataUpdateCoordinator for integration_blueprint.""" +"""DataUpdateCoordinator for belgiantrain.""" from __future__ import annotations @@ -7,26 +7,26 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .api import ( - IntegrationBlueprintApiClientAuthenticationError, - IntegrationBlueprintApiClientError, +from . import ( + BelgiantrainApiClientAuthenticationError, + BelgiantrainApiClientError, ) if TYPE_CHECKING: - from .data import IntegrationBlueprintConfigEntry + from . import BelgiantrainConfigEntry # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities -class BlueprintDataUpdateCoordinator(DataUpdateCoordinator): +class BelgiantrainDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching data from the API.""" - config_entry: IntegrationBlueprintConfigEntry + config_entry: BelgiantrainConfigEntry async def _async_update_data(self) -> Any: """Update data via library.""" try: return await self.config_entry.runtime_data.client.async_get_data() - except IntegrationBlueprintApiClientAuthenticationError as exception: + except BelgiantrainApiClientAuthenticationError as exception: raise ConfigEntryAuthFailed(exception) from exception - except IntegrationBlueprintApiClientError as exception: + except BelgiantrainApiClientError as exception: raise UpdateFailed(exception) from exception diff --git a/custom_components/belgiantrain/data.py b/custom_components/belgiantrain/data.py new file mode 100644 index 0000000..0363a80 --- /dev/null +++ b/custom_components/belgiantrain/data.py @@ -0,0 +1,27 @@ +"""Custom types for belgiantrain.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry + from homeassistant.loader import Integration + + from . import BelgiantrainApiClient + from .coordinator import BelgiantrainDataUpdateCoordinator + + +# TODO Create ConfigEntry type alias with API object +# TODO Rename type alias and update all entry annotations +type BelgiantrainConfigEntry = ConfigEntry[BelgiantrainData] + + +@dataclass +class BelgiantrainData: + """Data for the belgiantrain integration.""" + + client: BelgiantrainApiClient + coordinator: BelgiantrainDataUpdateCoordinator + integration: Integration diff --git a/custom_components/integration_blueprint/entity.py b/custom_components/belgiantrain/entity.py similarity index 68% rename from custom_components/integration_blueprint/entity.py rename to custom_components/belgiantrain/entity.py index dd75c31..a9d5dca 100644 --- a/custom_components/integration_blueprint/entity.py +++ b/custom_components/belgiantrain/entity.py @@ -1,4 +1,4 @@ -"""BlueprintEntity class.""" +"""BelgiantrainEntity class.""" from __future__ import annotations @@ -6,15 +6,15 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION -from .coordinator import BlueprintDataUpdateCoordinator +from . import BelgiantrainDataUpdateCoordinator -class IntegrationBlueprintEntity(CoordinatorEntity[BlueprintDataUpdateCoordinator]): - """BlueprintEntity class.""" +class BelgiantrainEntity(CoordinatorEntity[BelgiantrainDataUpdateCoordinator]): + """BelgiantrainEntity class.""" _attr_attribution = ATTRIBUTION - def __init__(self, coordinator: BlueprintDataUpdateCoordinator) -> None: + def __init__(self, coordinator: BelgiantrainDataUpdateCoordinator) -> None: """Initialize.""" super().__init__(coordinator) self._attr_unique_id = coordinator.config_entry.entry_id diff --git a/custom_components/integration_blueprint/sensor.py b/custom_components/belgiantrain/sensor.py similarity index 68% rename from custom_components/integration_blueprint/sensor.py rename to custom_components/belgiantrain/sensor.py index bd5f89b..f66c8e2 100644 --- a/custom_components/integration_blueprint/sensor.py +++ b/custom_components/belgiantrain/sensor.py @@ -1,4 +1,4 @@ -"""Sensor platform for integration_blueprint.""" +"""Sensor platform for belgiantrain.""" from __future__ import annotations @@ -6,19 +6,19 @@ from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from .entity import IntegrationBlueprintEntity +from .entity import BelgiantrainEntity if TYPE_CHECKING: from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback - from .coordinator import BlueprintDataUpdateCoordinator - from .data import IntegrationBlueprintConfigEntry + from . import BelgiantrainDataUpdateCoordinator + from .data import BelgiantrainConfigEntry ENTITY_DESCRIPTIONS = ( SensorEntityDescription( - key="integration_blueprint", - name="Integration Sensor", + key="belgiantrain", + name="Belgiantrain Sensor", icon="mdi:format-quote-close", ), ) @@ -26,12 +26,12 @@ async def async_setup_entry( hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` - entry: IntegrationBlueprintConfigEntry, + entry: BelgiantrainConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor platform.""" async_add_entities( - IntegrationBlueprintSensor( + BelgiantrainSensor( coordinator=entry.runtime_data.coordinator, entity_description=entity_description, ) @@ -39,12 +39,12 @@ async def async_setup_entry( ) -class IntegrationBlueprintSensor(IntegrationBlueprintEntity, SensorEntity): - """integration_blueprint Sensor class.""" +class BelgiantrainSensor(BelgiantrainEntity, SensorEntity): + """belgiantrain Sensor class.""" def __init__( self, - coordinator: BlueprintDataUpdateCoordinator, + coordinator: BelgiantrainDataUpdateCoordinator, entity_description: SensorEntityDescription, ) -> None: """Initialize the sensor class.""" diff --git a/custom_components/integration_blueprint/switch.py b/custom_components/belgiantrain/switch.py similarity index 75% rename from custom_components/integration_blueprint/switch.py rename to custom_components/belgiantrain/switch.py index 7629220..fc0d323 100644 --- a/custom_components/integration_blueprint/switch.py +++ b/custom_components/belgiantrain/switch.py @@ -1,4 +1,4 @@ -"""Switch platform for integration_blueprint.""" +"""Switch platform for belgiantrain.""" from __future__ import annotations @@ -6,19 +6,19 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from .entity import IntegrationBlueprintEntity +from .entity import BelgiantrainEntity if TYPE_CHECKING: from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback - from .coordinator import BlueprintDataUpdateCoordinator - from .data import IntegrationBlueprintConfigEntry + from . import BelgiantrainDataUpdateCoordinator + from .data import BelgiantrainConfigEntry ENTITY_DESCRIPTIONS = ( SwitchEntityDescription( - key="integration_blueprint", - name="Integration Switch", + key="belgiantrain", + name="Belgiantrain Switch", icon="mdi:format-quote-close", ), ) @@ -26,12 +26,12 @@ async def async_setup_entry( hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` - entry: IntegrationBlueprintConfigEntry, + entry: BelgiantrainConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the switch platform.""" async_add_entities( - IntegrationBlueprintSwitch( + BelgiantrainSwitch( coordinator=entry.runtime_data.coordinator, entity_description=entity_description, ) @@ -39,12 +39,12 @@ async def async_setup_entry( ) -class IntegrationBlueprintSwitch(IntegrationBlueprintEntity, SwitchEntity): - """integration_blueprint switch class.""" +class BelgiantrainSwitch(BelgiantrainEntity, SwitchEntity): + """belgiantrain switch class.""" def __init__( self, - coordinator: BlueprintDataUpdateCoordinator, + coordinator: BelgiantrainDataUpdateCoordinator, entity_description: SwitchEntityDescription, ) -> None: """Initialize the switch class.""" diff --git a/custom_components/integration_blueprint/__init__.py b/custom_components/integration_blueprint/__init__.py deleted file mode 100644 index be346b3..0000000 --- a/custom_components/integration_blueprint/__init__.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Custom integration to integrate integration_blueprint with Home Assistant. - -For more details about this integration, please refer to -https://github.com/ludeeus/integration_blueprint -""" - -from __future__ import annotations - -from datetime import timedelta -from typing import TYPE_CHECKING - -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.loader import async_get_loaded_integration - -from .api import IntegrationBlueprintApiClient -from .const import DOMAIN, LOGGER -from .coordinator import BlueprintDataUpdateCoordinator -from .data import IntegrationBlueprintData - -if TYPE_CHECKING: - from homeassistant.core import HomeAssistant - - from .data import IntegrationBlueprintConfigEntry - -PLATFORMS: list[Platform] = [ - Platform.SENSOR, - Platform.BINARY_SENSOR, - Platform.SWITCH, -] - - -# https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry -async def async_setup_entry( - hass: HomeAssistant, - entry: IntegrationBlueprintConfigEntry, -) -> bool: - """Set up this integration using UI.""" - coordinator = BlueprintDataUpdateCoordinator( - hass=hass, - logger=LOGGER, - name=DOMAIN, - update_interval=timedelta(hours=1), - ) - entry.runtime_data = IntegrationBlueprintData( - client=IntegrationBlueprintApiClient( - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], - session=async_get_clientsession(hass), - ), - integration=async_get_loaded_integration(hass, entry.domain), - coordinator=coordinator, - ) - - # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities - await coordinator.async_config_entry_first_refresh() - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(async_reload_entry)) - - return True - - -async def async_unload_entry( - hass: HomeAssistant, - entry: IntegrationBlueprintConfigEntry, -) -> bool: - """Handle removal of an entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def async_reload_entry( - hass: HomeAssistant, - entry: IntegrationBlueprintConfigEntry, -) -> None: - """Reload config entry.""" - await async_unload_entry(hass, entry) - await async_setup_entry(hass, entry) diff --git a/custom_components/integration_blueprint/const.py b/custom_components/integration_blueprint/const.py deleted file mode 100644 index ff45085..0000000 --- a/custom_components/integration_blueprint/const.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Constants for integration_blueprint.""" - -from logging import Logger, getLogger - -LOGGER: Logger = getLogger(__package__) - -DOMAIN = "integration_blueprint" -ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" diff --git a/custom_components/integration_blueprint/data.py b/custom_components/integration_blueprint/data.py deleted file mode 100644 index cdeb1ea..0000000 --- a/custom_components/integration_blueprint/data.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Custom types for integration_blueprint.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from homeassistant.config_entries import ConfigEntry - from homeassistant.loader import Integration - - from .api import IntegrationBlueprintApiClient - from .coordinator import BlueprintDataUpdateCoordinator - - -type IntegrationBlueprintConfigEntry = ConfigEntry[IntegrationBlueprintData] - - -@dataclass -class IntegrationBlueprintData: - """Data for the Blueprint integration.""" - - client: IntegrationBlueprintApiClient - coordinator: BlueprintDataUpdateCoordinator - integration: Integration