Skip to content
Snippets Groups Projects
Unverified Commit c03fdd5d authored by Aidan Timson's avatar Aidan Timson Committed by GitHub
Browse files

Add Azure DevOps coordinator and entity description (#54978)


Co-authored-by: default avatarJoakim Sørensen <hi@ludeeus.dev>
Co-authored-by: default avatarLudeeus <ludeeus@ludeeus.dev>
parent 1910c056
No related branches found
No related tags found
No related merge requests found
"""Support for Azure DevOps.""" """Support for Azure DevOps."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
import logging import logging
from typing import Final
from aioazuredevops.builds import DevOpsBuild
from aioazuredevops.client import DevOpsClient from aioazuredevops.client import DevOpsClient
from aioazuredevops.core import DevOpsProject
import aiohttp import aiohttp
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DATA_AZURE_DEVOPS_CLIENT, DOMAIN from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"] PLATFORMS = ["sensor"]
BUILDS_QUERY: Final = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1"
@dataclass
class AzureDevOpsEntityDescription(EntityDescription):
"""Class describing Azure DevOps entities."""
organization: str = ""
project: DevOpsProject = None
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Azure DevOps from a config entry.""" """Set up Azure DevOps from a config entry."""
client = DevOpsClient() client = DevOpsClient()
try: if entry.data.get(CONF_PAT) is not None:
if entry.data[CONF_PAT] is not None: await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG])
await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG]) if not client.authorized:
if not client.authorized: raise ConfigEntryAuthFailed(
raise ConfigEntryAuthFailed( "Could not authorize with Azure DevOps. You will need to update your token"
"Could not authorize with Azure DevOps. You may need to update your token" )
)
await client.get_project(entry.data[CONF_ORG], entry.data[CONF_PROJECT]) project = await client.get_project(
except aiohttp.ClientError as exception: entry.data[CONF_ORG],
_LOGGER.warning(exception) entry.data[CONF_PROJECT],
raise ConfigEntryNotReady from exception )
instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}" async def async_update_data() -> list[DevOpsBuild]:
hass.data.setdefault(instance_key, {})[DATA_AZURE_DEVOPS_CLIENT] = client """Fetch data from Azure DevOps."""
# Setup components try:
return await client.get_builds(
entry.data[CONF_ORG],
entry.data[CONF_PROJECT],
BUILDS_QUERY,
)
except (aiohttp.ClientError, aiohttp.ClientError) as exception:
raise UpdateFailed from exception
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"{DOMAIN}_coordinator",
update_method=async_update_data,
update_interval=timedelta(seconds=300),
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator, project
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True return True
...@@ -45,36 +86,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ...@@ -45,36 +86,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Azure DevOps config entry.""" """Unload Azure DevOps config entry."""
del hass.data[f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"] unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) del hass.data[DOMAIN][entry.entry_id]
return unload_ok
class AzureDevOpsEntity(Entity): class AzureDevOpsEntity(CoordinatorEntity):
"""Defines a base Azure DevOps entity.""" """Defines a base Azure DevOps entity."""
def __init__(self, organization: str, project: str, name: str, icon: str) -> None: coordinator: DataUpdateCoordinator[list[DevOpsBuild]]
entity_description: AzureDevOpsEntityDescription
def __init__(
self,
coordinator: DataUpdateCoordinator[list[DevOpsBuild]],
entity_description: AzureDevOpsEntityDescription,
) -> None:
"""Initialize the Azure DevOps entity.""" """Initialize the Azure DevOps entity."""
self._attr_name = name super().__init__(coordinator)
self._attr_icon = icon self.entity_description = entity_description
self.organization = organization self._attr_unique_id: str = "_".join(
self.project = project [entity_description.organization, entity_description.key]
)
async def async_update(self) -> None: self._organization: str = entity_description.organization
"""Update Azure DevOps entity.""" self._project_name: str = entity_description.project.name
if await self._azure_devops_update():
self._attr_available = True
else:
if self._attr_available:
_LOGGER.debug(
"An error occurred while updating Azure DevOps sensor",
exc_info=True,
)
self._attr_available = False
async def _azure_devops_update(self) -> bool:
"""Update Azure DevOps entity."""
raise NotImplementedError()
class AzureDevOpsDeviceEntity(AzureDevOpsEntity): class AzureDevOpsDeviceEntity(AzureDevOpsEntity):
...@@ -85,7 +121,7 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): ...@@ -85,7 +121,7 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity):
"""Return device information about this Azure DevOps instance.""" """Return device information about this Azure DevOps instance."""
return DeviceInfo( return DeviceInfo(
entry_type="service", entry_type="service",
identifiers={(DOMAIN, self.organization, self.project)}, # type: ignore identifiers={(DOMAIN, self._organization, self._project_name)}, # type: ignore
manufacturer=self.organization, manufacturer=self._organization,
name=self.project, name=self._project_name,
) )
"""Constants for the Azure DevOps integration.""" """Constants for the Azure DevOps integration."""
DOMAIN = "azure_devops" DOMAIN = "azure_devops"
DATA_AZURE_DEVOPS_CLIENT = "azure_devops_client"
DATA_ORG = "organization"
DATA_PROJECT = "project"
DATA_PAT = "personal_access_token"
CONF_ORG = "organization" CONF_ORG = "organization"
CONF_PROJECT = "project" CONF_PROJECT = "project"
CONF_PAT = "personal_access_token" CONF_PAT = "personal_access_token"
"""Support for Azure DevOps sensors.""" """Support for Azure DevOps sensors."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from dataclasses import dataclass
import logging from typing import Any, Callable
from aioazuredevops.builds import DevOpsBuild from aioazuredevops.builds import DevOpsBuild
from aioazuredevops.client import DevOpsClient
import aiohttp from homeassistant.components.azure_devops import (
AzureDevOpsDeviceEntity,
from homeassistant.components.azure_devops import AzureDevOpsDeviceEntity AzureDevOpsEntityDescription,
from homeassistant.components.azure_devops.const import (
CONF_ORG,
CONF_PROJECT,
DATA_AZURE_DEVOPS_CLIENT,
DATA_ORG,
DATA_PROJECT,
DOMAIN,
) )
from homeassistant.components.sensor import SensorEntity from homeassistant.components.azure_devops.const import CONF_ORG, DOMAIN
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
@dataclass
class AzureDevOpsSensorEntityDescriptionMixin:
"""Mixin class for required Azure DevOps sensor description keys."""
_LOGGER = logging.getLogger(__name__) build_key: int
SCAN_INTERVAL = timedelta(seconds=300)
PARALLEL_UPDATES = 4
BUILDS_QUERY = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" @dataclass
class AzureDevOpsSensorEntityDescription(
AzureDevOpsEntityDescription,
SensorEntityDescription,
AzureDevOpsSensorEntityDescriptionMixin,
):
"""Class describing Azure DevOps sensor entities."""
attrs: Callable[[DevOpsBuild], Any] = round
value: Callable[[DevOpsBuild], StateType] = round
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up Azure DevOps sensor based on a config entry.""" """Set up Azure DevOps sensor based on a config entry."""
instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}" coordinator, project = hass.data[DOMAIN][entry.entry_id]
client = hass.data[instance_key][DATA_AZURE_DEVOPS_CLIENT]
organization = entry.data[DATA_ORG] sensors = [
project = entry.data[DATA_PROJECT] AzureDevOpsSensor(
sensors = [] coordinator,
AzureDevOpsSensorEntityDescription(
try: key=f"{build.project.id}_{build.definition.id}_latest_build",
builds: list[DevOpsBuild] = await client.get_builds( name=f"{build.project.name} {build.definition.name} Latest Build",
organization, project, BUILDS_QUERY icon="mdi:pipe",
) attrs=lambda build: {
except aiohttp.ClientError as exception: "definition_id": build.definition.id,
_LOGGER.warning(exception) "definition_name": build.definition.name,
raise PlatformNotReady from exception "id": build.id,
"reason": build.reason,
for build in builds: "result": build.result,
sensors.append( "source_branch": build.source_branch,
AzureDevOpsLatestBuildSensor(client, organization, project, build) "source_version": build.source_version,
"status": build.status,
"url": build.links.web,
"queue_time": build.queue_time,
"start_time": build.start_time,
"finish_time": build.finish_time,
},
build_key=key,
organization=entry.data[CONF_ORG],
project=project,
value=lambda build: build.build_number,
),
) )
for key, build in enumerate(coordinator.data)
]
async_add_entities(sensors, True) async_add_entities(sensors, True)
class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity): class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity):
"""Defines a Azure DevOps sensor.""" """Define a Azure DevOps sensor."""
def __init__( entity_description: AzureDevOpsSensorEntityDescription
self,
client: DevOpsClient, @property
organization: str, def native_value(self) -> StateType:
project: str, """Return the state."""
key: str, build: DevOpsBuild = self.coordinator.data[self.entity_description.build_key]
name: str, return self.entity_description.value(build)
icon: str,
measurement: str = "",
unit_of_measurement: str = "",
) -> None:
"""Initialize Azure DevOps sensor."""
self._attr_native_unit_of_measurement = unit_of_measurement
self.client = client
self.organization = organization
self.project = project
self._attr_unique_id = "_".join([organization, key])
super().__init__(organization, project, name, icon)
class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor):
"""Defines a Azure DevOps card count sensor."""
def __init__(
self, client: DevOpsClient, organization: str, project: str, build: DevOpsBuild
) -> None:
"""Initialize Azure DevOps sensor."""
self.build: DevOpsBuild = build
super().__init__(
client,
organization,
project,
f"{build.project.id}_{build.definition.id}_latest_build",
f"{build.project.name} {build.definition.name} Latest Build",
"mdi:pipe",
)
async def _azure_devops_update(self) -> bool: @property
"""Update Azure DevOps entity.""" def extra_state_attributes(self) -> dict[str, Any]:
try: """Return the state attributes of the entity."""
build: DevOpsBuild = await self.client.get_build( build: DevOpsBuild = self.coordinator.data[self.entity_description.build_key]
self.organization, self.project, self.build.id return self.entity_description.attrs(build)
)
except aiohttp.ClientError as exception:
_LOGGER.warning(exception)
self._attr_available = False
return False
self._attr_native_value = build.build_number
self._attr_extra_state_attributes = {
"definition_id": build.definition.id,
"definition_name": build.definition.name,
"id": build.id,
"reason": build.reason,
"result": build.result,
"source_branch": build.source_branch,
"source_version": build.source_version,
"status": build.status,
"url": build.links.web,
"queue_time": build.queue_time,
"start_time": build.start_time,
"finish_time": build.finish_time,
}
self._attr_available = True
return True
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment