Add Azure DevOps coordinator and entity description (#54978)

Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
Co-authored-by: Ludeeus <ludeeus@ludeeus.dev>
This commit is contained in:
Aidan Timson 2021-11-10 10:49:04 +00:00 committed by GitHub
parent 1910c0566c
commit c03fdd5da6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 148 additions and 149 deletions

View file

@ -1,43 +1,84 @@
"""Support for Azure DevOps."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Final
from aioazuredevops.builds import DevOpsBuild
from aioazuredevops.client import DevOpsClient
from aioazuredevops.core import DevOpsProject
import aiohttp
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.exceptions import ConfigEntryAuthFailed
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__)
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:
"""Set up Azure DevOps from a config entry."""
client = DevOpsClient()
try:
if entry.data[CONF_PAT] is not None:
if entry.data.get(CONF_PAT) is not None:
await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG])
if not client.authorized:
raise ConfigEntryAuthFailed(
"Could not authorize with Azure DevOps. You may need to update your token"
"Could not authorize with Azure DevOps. You will need to update your token"
)
await client.get_project(entry.data[CONF_ORG], entry.data[CONF_PROJECT])
except aiohttp.ClientError as exception:
_LOGGER.warning(exception)
raise ConfigEntryNotReady from exception
instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"
hass.data.setdefault(instance_key, {})[DATA_AZURE_DEVOPS_CLIENT] = client
project = await client.get_project(
entry.data[CONF_ORG],
entry.data[CONF_PROJECT],
)
async def async_update_data() -> list[DevOpsBuild]:
"""Fetch data from Azure DevOps."""
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
# Setup components
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
@ -45,36 +86,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Azure DevOps config entry."""
del hass.data[f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"]
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
del hass.data[DOMAIN][entry.entry_id]
return unload_ok
class AzureDevOpsEntity(Entity):
class AzureDevOpsEntity(CoordinatorEntity):
"""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."""
self._attr_name = name
self._attr_icon = icon
self.organization = organization
self.project = project
async def async_update(self) -> None:
"""Update Azure DevOps entity."""
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,
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id: str = "_".join(
[entity_description.organization, entity_description.key]
)
self._attr_available = False
async def _azure_devops_update(self) -> bool:
"""Update Azure DevOps entity."""
raise NotImplementedError()
self._organization: str = entity_description.organization
self._project_name: str = entity_description.project.name
class AzureDevOpsDeviceEntity(AzureDevOpsEntity):
@ -85,7 +121,7 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity):
"""Return device information about this Azure DevOps instance."""
return DeviceInfo(
entry_type="service",
identifiers={(DOMAIN, self.organization, self.project)}, # type: ignore
manufacturer=self.organization,
name=self.project,
identifiers={(DOMAIN, self._organization, self._project_name)}, # type: ignore
manufacturer=self._organization,
name=self._project_name,
)

View file

@ -1,11 +1,6 @@
"""Constants for the Azure DevOps integration."""
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_PROJECT = "project"
CONF_PAT = "personal_access_token"

View file

@ -1,114 +1,56 @@
"""Support for Azure DevOps sensors."""
from __future__ import annotations
from datetime import timedelta
import logging
from dataclasses import dataclass
from typing import Any, Callable
from aioazuredevops.builds import DevOpsBuild
from aioazuredevops.client import DevOpsClient
import aiohttp
from homeassistant.components.azure_devops import AzureDevOpsDeviceEntity
from homeassistant.components.azure_devops.const import (
CONF_ORG,
CONF_PROJECT,
DATA_AZURE_DEVOPS_CLIENT,
DATA_ORG,
DATA_PROJECT,
DOMAIN,
from homeassistant.components.azure_devops import (
AzureDevOpsDeviceEntity,
AzureDevOpsEntityDescription,
)
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.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=300)
PARALLEL_UPDATES = 4
@dataclass
class AzureDevOpsSensorEntityDescriptionMixin:
"""Mixin class for required Azure DevOps sensor description keys."""
BUILDS_QUERY = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1"
build_key: int
@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(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Azure DevOps sensor based on a config entry."""
instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"
client = hass.data[instance_key][DATA_AZURE_DEVOPS_CLIENT]
organization = entry.data[DATA_ORG]
project = entry.data[DATA_PROJECT]
sensors = []
coordinator, project = hass.data[DOMAIN][entry.entry_id]
try:
builds: list[DevOpsBuild] = await client.get_builds(
organization, project, BUILDS_QUERY
)
except aiohttp.ClientError as exception:
_LOGGER.warning(exception)
raise PlatformNotReady from exception
for build in builds:
sensors.append(
AzureDevOpsLatestBuildSensor(client, organization, project, build)
)
async_add_entities(sensors, True)
class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity):
"""Defines a Azure DevOps sensor."""
def __init__(
self,
client: DevOpsClient,
organization: str,
project: str,
key: str,
name: str,
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:
"""Update Azure DevOps entity."""
try:
build: DevOpsBuild = await self.client.get_build(
self.organization, self.project, self.build.id
)
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 = {
sensors = [
AzureDevOpsSensor(
coordinator,
AzureDevOpsSensorEntityDescription(
key=f"{build.project.id}_{build.definition.id}_latest_build",
name=f"{build.project.name} {build.definition.name} Latest Build",
icon="mdi:pipe",
attrs=lambda build: {
"definition_id": build.definition.id,
"definition_name": build.definition.name,
"id": build.id,
@ -121,6 +63,32 @@ class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor):
"queue_time": build.queue_time,
"start_time": build.start_time,
"finish_time": build.finish_time,
}
self._attr_available = True
return True
},
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)
class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity):
"""Define a Azure DevOps sensor."""
entity_description: AzureDevOpsSensorEntityDescription
@property
def native_value(self) -> StateType:
"""Return the state."""
build: DevOpsBuild = self.coordinator.data[self.entity_description.build_key]
return self.entity_description.value(build)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the entity."""
build: DevOpsBuild = self.coordinator.data[self.entity_description.build_key]
return self.entity_description.attrs(build)