"""Support for Azure DevOps sensors.""" from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import datetime import logging from typing import Any from aioazuredevops.helper import WorkItemState, WorkItemTypeAndState from aioazuredevops.models.build import Build from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from . import AzureDevOpsConfigEntry from .coordinator import AzureDevOpsDataUpdateCoordinator from .entity import AzureDevOpsEntity _LOGGER = logging.getLogger(__name__) @dataclass(frozen=True, kw_only=True) class AzureDevOpsBuildSensorEntityDescription(SensorEntityDescription): """Class describing Azure DevOps build sensor entities.""" attr_fn: Callable[[Build], dict[str, Any] | None] = lambda _: None value_fn: Callable[[Build], datetime | StateType] @dataclass(frozen=True, kw_only=True) class AzureDevOpsWorkItemSensorEntityDescription(SensorEntityDescription): """Class describing Azure DevOps work item sensor entities.""" value_fn: Callable[[WorkItemState], datetime | StateType] BASE_BUILD_SENSOR_DESCRIPTIONS: tuple[AzureDevOpsBuildSensorEntityDescription, ...] = ( # Attributes are deprecated in 2024.7 and can be removed in 2025.1 AzureDevOpsBuildSensorEntityDescription( key="latest_build", translation_key="latest_build", attr_fn=lambda build: { "definition_id": (build.definition.build_id if build.definition else None), "definition_name": (build.definition.name if build.definition else None), "id": build.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 if build.links else None, "queue_time": build.queue_time, "start_time": build.start_time, "finish_time": build.finish_time, }, value_fn=lambda build: build.build_number, ), AzureDevOpsBuildSensorEntityDescription( key="build_id", translation_key="build_id", entity_registry_visible_default=False, value_fn=lambda build: build.build_id, ), AzureDevOpsBuildSensorEntityDescription( key="reason", translation_key="reason", entity_registry_visible_default=False, value_fn=lambda build: build.reason, ), AzureDevOpsBuildSensorEntityDescription( key="result", translation_key="result", entity_registry_visible_default=False, value_fn=lambda build: build.result, ), AzureDevOpsBuildSensorEntityDescription( key="source_branch", translation_key="source_branch", entity_registry_enabled_default=False, entity_registry_visible_default=False, value_fn=lambda build: build.source_branch, ), AzureDevOpsBuildSensorEntityDescription( key="source_version", translation_key="source_version", entity_registry_visible_default=False, value_fn=lambda build: build.source_version, ), AzureDevOpsBuildSensorEntityDescription( key="queue_time", translation_key="queue_time", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, entity_registry_visible_default=False, value_fn=lambda build: parse_datetime(build.queue_time), ), AzureDevOpsBuildSensorEntityDescription( key="start_time", translation_key="start_time", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_visible_default=False, value_fn=lambda build: parse_datetime(build.start_time), ), AzureDevOpsBuildSensorEntityDescription( key="finish_time", translation_key="finish_time", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_visible_default=False, value_fn=lambda build: parse_datetime(build.finish_time), ), AzureDevOpsBuildSensorEntityDescription( key="url", translation_key="url", value_fn=lambda build: build.links.web if build.links else None, ), ) BASE_WORK_ITEM_SENSOR_DESCRIPTIONS: tuple[ AzureDevOpsWorkItemSensorEntityDescription, ... ] = ( AzureDevOpsWorkItemSensorEntityDescription( key="work_item_count", translation_key="work_item_count", value_fn=lambda work_item_state: len(work_item_state.work_items), ), ) def parse_datetime(value: str | None) -> datetime | None: """Parse datetime string.""" if value is None: return None return dt_util.parse_datetime(value) async def async_setup_entry( hass: HomeAssistant, entry: AzureDevOpsConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Azure DevOps sensor based on a config entry.""" coordinator = entry.runtime_data initial_builds: list[Build] = coordinator.data.builds entities: list[SensorEntity] = [ AzureDevOpsBuildSensor( coordinator, description, key, ) for description in BASE_BUILD_SENSOR_DESCRIPTIONS for key, build in enumerate(initial_builds) if build.project and build.definition ] entities.extend( AzureDevOpsWorkItemSensor( coordinator, description, key, state_key, ) for description in BASE_WORK_ITEM_SENSOR_DESCRIPTIONS for key, work_item_type_state in enumerate(coordinator.data.work_items) for state_key, _ in enumerate(work_item_type_state.state_items) ) async_add_entities(entities) class AzureDevOpsBuildSensor(AzureDevOpsEntity, SensorEntity): """Define a Azure DevOps build sensor.""" entity_description: AzureDevOpsBuildSensorEntityDescription def __init__( self, coordinator: AzureDevOpsDataUpdateCoordinator, description: AzureDevOpsBuildSensorEntityDescription, item_key: int, ) -> None: """Initialize.""" super().__init__(coordinator) self.entity_description = description self.item_key = item_key self._attr_unique_id = ( f"{coordinator.data.organization}_" f"{coordinator.data.project.id}_" f"{self.build.definition.build_id}_" f"{description.key}" ) self._attr_translation_placeholders = { "definition_name": self.build.definition.name } @property def build(self) -> Build: """Return the build.""" return self.coordinator.data.builds[self.item_key] @property def native_value(self) -> datetime | StateType: """Return the state.""" return self.entity_description.value_fn(self.build) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes of the entity.""" return self.entity_description.attr_fn(self.build) class AzureDevOpsWorkItemSensor(AzureDevOpsEntity, SensorEntity): """Define a Azure DevOps work item sensor.""" entity_description: AzureDevOpsWorkItemSensorEntityDescription def __init__( self, coordinator: AzureDevOpsDataUpdateCoordinator, description: AzureDevOpsWorkItemSensorEntityDescription, wits_key: int, state_key: int, ) -> None: """Initialize.""" super().__init__(coordinator) self.entity_description = description self.wits_key = wits_key self.state_key = state_key self._attr_unique_id = ( f"{coordinator.data.organization}_" f"{coordinator.data.project.id}_" f"{self.work_item_type.name}_" f"{self.work_item_state.name}_" f"{description.key}" ) self._attr_translation_placeholders = { "item_type": self.work_item_type.name, "item_state": self.work_item_state.name, } @property def work_item_type(self) -> WorkItemTypeAndState: """Return the work item.""" return self.coordinator.data.work_items[self.wits_key] @property def work_item_state(self) -> WorkItemState: """Return the work item state.""" return self.work_item_type.state_items[self.state_key] @property def native_value(self) -> datetime | StateType: """Return the state.""" return self.entity_description.value_fn(self.work_item_state)