Skip to content
Merged
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
10 changes: 0 additions & 10 deletions homeassistant/components/airpatrol/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,11 @@ def __init__(
super().__init__(coordinator, unit_id)
self._attr_unique_id = f"{coordinator.config_entry.unique_id}-{unit_id}"

@property
def climate_data(self) -> dict[str, Any]:
"""Return the climate data."""
return self.device_data.get("climate") or {}

@property
def params(self) -> dict[str, Any]:
"""Return the current parameters for the climate entity."""
return self.climate_data.get("ParametersData") or {}

@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and bool(self.climate_data)

@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/airpatrol/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
DOMAIN = "airpatrol"

LOGGER = logging.getLogger(__package__)
PLATFORMS = [Platform.CLIMATE]
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=1)

AIRPATROL_ERRORS = (AirPatrolAuthenticationError, AirPatrolError)
12 changes: 11 additions & 1 deletion homeassistant/components/airpatrol/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,17 @@ def device_data(self) -> dict[str, Any]:
"""Return the device data."""
return self.coordinator.data[self._unit_id]

@property
def climate_data(self) -> dict[str, Any]:
"""Return the climate data for this unit."""
return self.device_data["climate"]

@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._unit_id in self.coordinator.data
return (
super().available
and self._unit_id in self.coordinator.data
and "climate" in self.device_data
and self.climate_data is not None
)
89 changes: 89 additions & 0 deletions homeassistant/components/airpatrol/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Sensors for AirPatrol integration."""

from __future__ import annotations

from dataclasses import dataclass

from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from . import AirPatrolConfigEntry
from .coordinator import AirPatrolDataUpdateCoordinator
from .entity import AirPatrolEntity

PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class AirPatrolSensorEntityDescription(SensorEntityDescription):
"""Describes AirPatrol sensor entity."""

data_field: str


SENSOR_DESCRIPTIONS = (
AirPatrolSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
data_field="RoomTemp",
),
AirPatrolSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
data_field="RoomHumidity",
),
)
Comment thread
antondalgren marked this conversation as resolved.


async def async_setup_entry(
hass: HomeAssistant,
config_entry: AirPatrolConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up AirPatrol sensors."""
coordinator = config_entry.runtime_data
units = coordinator.data

async_add_entities(
AirPatrolSensor(coordinator, unit_id, description)
for unit_id, unit in units.items()
for description in SENSOR_DESCRIPTIONS
if "climate" in unit and unit["climate"] is not None
)


class AirPatrolSensor(AirPatrolEntity, SensorEntity):
"""AirPatrol sensor entity."""

entity_description: AirPatrolSensorEntityDescription

def __init__(
self,
coordinator: AirPatrolDataUpdateCoordinator,
unit_id: str,
description: AirPatrolSensorEntityDescription,
) -> None:
"""Initialize AirPatrol sensor."""
super().__init__(coordinator, unit_id)
self.entity_description = description
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}-{unit_id}-{description.key}"
)

@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
if value := self.climate_data.get(self.entity_description.data_field):
return float(value)
return None
110 changes: 110 additions & 0 deletions tests/components/airpatrol/snapshots/test_sensor.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# serializer version: 1
# name: test_sensor_with_climate_data[sensor.living_room_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.living_room_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'airpatrol',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'test_user_id-test_unit_001-humidity',
'unit_of_measurement': '%',
})
# ---
# name: test_sensor_with_climate_data[sensor.living_room_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': 'living room Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.living_room_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '45.0',
})
# ---
# name: test_sensor_with_climate_data[sensor.living_room_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.living_room_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'airpatrol',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'test_user_id-test_unit_001-temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensor_with_climate_data[sensor.living_room_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'living room Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.living_room_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '22.5',
})
# ---
13 changes: 13 additions & 0 deletions tests/components/airpatrol/test_climate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Test the AirPatrol climate platform."""

from collections.abc import Generator
from datetime import timedelta
from typing import Any
from unittest.mock import patch

from airpatrol.api import AirPatrolAPI
from freezegun.api import FrozenDateTimeFactory
Expand Down Expand Up @@ -32,6 +34,7 @@
CONF_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
Expand All @@ -44,6 +47,16 @@
)


@pytest.fixture(autouse=True)
def override_platforms() -> Generator[None]:
"""Override the platforms to load for airpatrol."""
with patch(
"homeassistant.components.airpatrol.PLATFORMS",
[Platform.CLIMATE],
):
yield


@pytest.mark.parametrize(
"climate_data",
[
Expand Down
55 changes: 55 additions & 0 deletions tests/components/airpatrol/test_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Test the AirPatrol sensor platform."""

from collections.abc import Generator
from unittest.mock import patch

from airpatrol.api import AirPatrolAPI
import pytest

from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er

from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform


@pytest.fixture(autouse=True)
def override_platforms() -> Generator[None]:
"""Override the platforms to load for airpatrol."""
with patch(
"homeassistant.components.airpatrol.PLATFORMS",
[Platform.SENSOR],
):
yield


async def test_sensor_with_climate_data(
hass: HomeAssistant,
load_integration: MockConfigEntry,
get_client: AirPatrolAPI,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test sensor entities are created with climate data."""
await snapshot_platform(
hass,
entity_registry,
snapshot,
load_integration.entry_id,
)


@pytest.mark.parametrize(
"climate_data",
[
None,
],
)
async def test_sensor_with_no_climate_data(
hass: HomeAssistant,
load_integration: MockConfigEntry,
get_client: AirPatrolAPI,
entity_registry: er.EntityRegistry,
) -> None:
"""Test no sensor entities are created when no climate data is present."""
assert len(entity_registry.entities) == 0
Loading