Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 8 additions & 0 deletions homeassistant/components/blebox/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""BleBox binary sensor entities."""

from datetime import timedelta
Comment thread
bkobus-bbx marked this conversation as resolved.
Outdated

from blebox_uniapi.binary_sensor import BinarySensor as BinarySensorFeature
Comment thread
bkobus-bbx marked this conversation as resolved.
Outdated

from homeassistant.components.binary_sensor import (
Expand All @@ -13,11 +15,17 @@
from . import BleBoxConfigEntry
from .entity import BleBoxEntity

SCAN_INTERVAL = timedelta(seconds=5)
Comment thread
bkobus-bbx marked this conversation as resolved.
Outdated
Comment thread
bkobus-bbx marked this conversation as resolved.
Outdated

BINARY_SENSOR_TYPES = (
BinarySensorEntityDescription(
key="moisture",
device_class=BinarySensorDeviceClass.MOISTURE,
),
BinarySensorEntityDescription(
key="open",
device_class=BinarySensorDeviceClass.WINDOW,
),
Comment thread
bkobus-bbx marked this conversation as resolved.
)


Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/blebox/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
DEFAULT_HOST = "192.168.0.2"
DEFAULT_PORT = 80

OPEN_STATUS: dict[int, str] = {
0: "open",
1: "unclosed_or_unlocked",
2: "ajar",
3: "closed_but_unlocked",
4: "closed",
}
Comment thread
bkobus-bbx marked this conversation as resolved.

LIGHT_MAX_KELVINS = 6500 # 154 Mireds
LIGHT_MIN_KELVINS = 2700 # 370 Mireds
61 changes: 41 additions & 20 deletions homeassistant/components/blebox/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""BleBox sensor entities."""

from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta

import blebox_uniapi.sensor
Expand All @@ -26,95 +28,112 @@
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType

from . import BleBoxConfigEntry
from .const import OPEN_STATUS
from .entity import BleBoxEntity

SCAN_INTERVAL = timedelta(seconds=5)


SENSOR_TYPES = (
SensorEntityDescription(
@dataclass(kw_only=True, frozen=True)
class BleBoxSensorEntityDescription(SensorEntityDescription):
"""Describes a BleBox sensor entity."""

value_fn: Callable[[StateType], StateType] = lambda v: v
Comment thread
bkobus-bbx marked this conversation as resolved.
Comment thread
bkobus-bbx marked this conversation as resolved.


SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
BleBoxSensorEntityDescription(
key="pm1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="pm2_5",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="pm10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="powerConsumption",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=2,
icon="mdi:lightning-bolt",
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="wind",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="illuminance",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="forwardActiveEnergy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="reverseActiveEnergy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="reactivePower",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="activePower",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="apparentPower",
device_class=SensorDeviceClass.APPARENT_POWER,
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="frequency",
device_class=SensorDeviceClass.FREQUENCY,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
),
BleBoxSensorEntityDescription(
key="openStatus",
translation_key="open_status",
device_class=SensorDeviceClass.ENUM,
icon="mdi:window-open",
options=list(OPEN_STATUS.values()),
value_fn=lambda v: OPEN_STATUS.get(int(v)) if v is not None else None,
),
Comment thread
bkobus-bbx marked this conversation as resolved.
Comment thread
bkobus-bbx marked this conversation as resolved.
Comment thread
bkobus-bbx marked this conversation as resolved.
)


Expand All @@ -136,19 +155,21 @@ async def async_setup_entry(
class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEntity):
"""Representation of a BleBox sensor feature."""

entity_description: BleBoxSensorEntityDescription

def __init__(
self,
feature: blebox_uniapi.sensor.BaseSensor,
description: SensorEntityDescription,
description: BleBoxSensorEntityDescription,
) -> None:
"""Initialize a BleBox sensor feature."""
super().__init__(feature)
self.entity_description = description

@property
def native_value(self):
def native_value(self) -> StateType:
"""Return the state."""
return self._feature.native_value
return self.entity_description.value_fn(self._feature.native_value)

@property
def last_reset(self) -> datetime | None:
Expand Down
13 changes: 13 additions & 0 deletions homeassistant/components/blebox/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,18 @@
"title": "Set up your BleBox device"
}
}
},
"entity": {
"sensor": {
"open_status": {
"state": {
"ajar": "Ajar",
"closed": "Closed",
"closed_but_unlocked": "Closed but unlocked",
"open": "Open",
Comment thread
bkobus-bbx marked this conversation as resolved.
Outdated
"unclosed_or_unlocked": "Unclosed or unlocked"
Comment thread
bkobus-bbx marked this conversation as resolved.
}
}
}
}
}
63 changes: 62 additions & 1 deletion tests/components/blebox/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest

from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import ATTR_DEVICE_CLASS, STATE_ON
from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr

Expand All @@ -29,6 +29,22 @@ def airsensor_fixture() -> tuple[AsyncMock, str]:
return feature, "binary_sensor.my_rain_sensor_windrainsensor_0_rain"


@pytest.fixture(name="open_sensor")
def open_sensor_fixture() -> tuple[AsyncMock, str]:
"""Return a default open/window binary sensor fixture."""
feature: AsyncMock = mock_feature(
"binary_sensors",
blebox_uniapi.binary_sensor.Open,
unique_id="BleBox-openSensor-1afe34db9437-0.open",
full_name="openSensor-0.open",
device_class="open",
)
product = feature.product
type(product).name = PropertyMock(return_value="My open sensor")
type(product).model = PropertyMock(return_value="openSensor")
return feature, "binary_sensor.my_open_sensor_opensensor_0_open"


async def test_init(
rainsensor: AsyncMock, device_registry: dr.DeviceRegistry, hass: HomeAssistant
) -> None:
Expand All @@ -46,3 +62,48 @@ async def test_init(
device = device_registry.async_get(entry.device_id)

assert device.name == "My rain sensor"


async def test_open_sensor_init(
open_sensor: tuple[AsyncMock, str],
device_registry: dr.DeviceRegistry,
hass: HomeAssistant,
) -> None:
"""Test open/window binary sensor initialisation."""
_, entity_id = open_sensor
entry = await async_setup_entity(hass, entity_id)
assert entry.unique_id == "BleBox-openSensor-1afe34db9437-0.open"

state = hass.states.get(entity_id)
assert state.name == "My open sensor openSensor-0.open"
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.WINDOW

device = device_registry.async_get(entry.device_id)
assert device.name == "My open sensor"
assert device.model == "openSensor"


@pytest.mark.parametrize(
("is_open", "expected_state"),
[
pytest.param(True, STATE_ON, id="open"),
pytest.param(False, STATE_OFF, id="closed"),
],
)
async def test_open_sensor_state(
open_sensor: tuple[AsyncMock, str],
hass: HomeAssistant,
is_open: bool,
expected_state: str,
) -> None:
"""Test open/window binary sensor reports open and closed states correctly."""
feature_mock, entity_id = open_sensor

def set_state():
feature_mock.state = is_open

feature_mock.async_update = AsyncMock(side_effect=set_state)
await async_setup_entity(hass, entity_id)

state = hass.states.get(entity_id)
assert state.state == expected_state
75 changes: 75 additions & 0 deletions tests/components/blebox/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import blebox_uniapi
import pytest

from homeassistant.components.blebox.const import OPEN_STATUS
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import (
ATTR_DEVICE_CLASS,
Expand Down Expand Up @@ -154,3 +155,77 @@ def initial_update():
)

assert state.state == "49"


@pytest.fixture(name="open_status_sensor")
def open_status_sensor_fixture():
"""Return a default openStatus sensor mock."""
feature = mock_feature(
"sensors",
blebox_uniapi.sensor.GenericSensor,
unique_id="BleBox-openSensor-1afe34db9437-0.openStatus",
full_name="openSensor-0.openStatus",
device_class="openStatus",
native_value=None,
)
product = feature.product
type(product).name = PropertyMock(return_value="My open sensor")
type(product).model = PropertyMock(return_value="openSensor")
return (feature, "sensor.my_open_sensor_opensensor_0_openstatus")


async def test_open_status_sensor_init(open_status_sensor, hass: HomeAssistant) -> None:
"""Test openStatus sensor initial state is unknown."""
_, entity_id = open_status_sensor
await async_setup_entity(hass, entity_id)

state = hass.states.get(entity_id)
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
assert state.attributes["options"] == list(OPEN_STATUS.values())
Comment thread
bkobus-bbx marked this conversation as resolved.
Outdated
assert state.state == STATE_UNKNOWN


@pytest.mark.parametrize(
("raw_value", "expected_state"),
[
pytest.param(0, "open", id="0_open"),
pytest.param(1, "unclosed_or_unlocked", id="1_unclosed_or_unlocked"),
pytest.param(2, "ajar", id="2_ajar"),
pytest.param(3, "closed_but_unlocked", id="3_closed_but_unlocked"),
pytest.param(4, "closed", id="4_closed"),
],
)
async def test_open_status_sensor_value_mapping(
open_status_sensor,
hass: HomeAssistant,
raw_value: int,
expected_state: str,
) -> None:
"""Test that each raw numeric openStatus value maps to the correct string state."""
feature_mock, entity_id = open_status_sensor

def set_value():
feature_mock.native_value = raw_value

feature_mock.async_update = AsyncMock(side_effect=set_value)
await async_setup_entity(hass, entity_id)

state = hass.states.get(entity_id)
assert state.state == expected_state
assert state.state in OPEN_STATUS.values()


async def test_open_status_sensor_none_value(
open_status_sensor, hass: HomeAssistant
) -> None:
"""Test that a None native_value yields an unknown state."""
feature_mock, entity_id = open_status_sensor

def set_none():
feature_mock.native_value = None

feature_mock.async_update = AsyncMock(side_effect=set_none)
await async_setup_entity(hass, entity_id)

state = hass.states.get(entity_id)
assert state.state == STATE_UNKNOWN