diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 503717c61e4..04e4a0b873d 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -52,6 +52,13 @@ class SensiboDeviceBinarySensorEntityDescription( """Describes Sensibo Motion sensor entity.""" +FILTER_CLEAN_REQUIRED_DESCRIPTION = SensiboDeviceBinarySensorEntityDescription( + key="filter_clean", + device_class=BinarySensorDeviceClass.PROBLEM, + name="Filter Clean Required", + value_fn=lambda data: data.filter_clean, +) + MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( SensiboMotionBinarySensorEntityDescription( key="alive", @@ -85,6 +92,7 @@ MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, .. icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, ), + FILTER_CLEAN_REQUIRED_DESCRIPTION, ) PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( @@ -127,6 +135,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( icon="mdi:connection", value_fn=lambda data: data.pure_prime_integration, ), + FILTER_CLEAN_REQUIRED_DESCRIPTION, ) diff --git a/homeassistant/components/sensibo/button.py b/homeassistant/components/sensibo/button.py new file mode 100644 index 00000000000..97ae6321f7e --- /dev/null +++ b/homeassistant/components/sensibo/button.py @@ -0,0 +1,68 @@ +"""Button platform for Sensibo integration.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboDeviceBaseEntity + +PARALLEL_UPDATES = 0 + +DEVICE_BUTTON_TYPES: ButtonEntityDescription = ButtonEntityDescription( + key="reset_filter", + name="Reset Filter", + icon="mdi:air-filter", + entity_category=EntityCategory.CONFIG, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo binary sensor platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[SensiboDeviceButton] = [] + + entities.extend( + SensiboDeviceButton(coordinator, device_id, DEVICE_BUTTON_TYPES) + for device_id, device_data in coordinator.data.parsed.items() + ) + + async_add_entities(entities) + + +class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity): + """Representation of a Sensibo Device Binary Sensor.""" + + entity_description: ButtonEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: ButtonEntityDescription, + ) -> None: + """Initiate Sensibo Device Button.""" + super().__init__( + coordinator, + device_id, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = f"{self.device_data.name} {entity_description.name}" + + async def async_press(self) -> None: + """Press the button.""" + result = await self.async_send_command("reset_filter") + if result["status"] == "success": + await self.coordinator.async_request_refresh() + return + raise HomeAssistantError(f"Could not set calibration for device {self.name}") diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 736d663b144..d6dbe957def 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -14,6 +14,7 @@ DEFAULT_SCAN_INTERVAL = 60 DOMAIN = "sensibo" PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index bf70f499ec6..ac2ec24fac1 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -106,6 +106,8 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): self._device_id, params, ) + if command == "reset_filter": + result = await self._client.async_reset_filter(self._device_id) return result diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 259a24ab876..fad22fdd677 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -63,6 +63,15 @@ class SensiboDeviceSensorEntityDescription( """Describes Sensibo Motion sensor entity.""" +FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription( + key="filter_last_reset", + device_class=SensorDeviceClass.TIMESTAMP, + name="Filter Last Reset", + icon="mdi:timer", + value_fn=lambda data: data.filter_last_reset, + extra_fn=None, +) + MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( SensiboMotionSensorEntityDescription( key="rssi", @@ -122,6 +131,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( value_fn=lambda data: data.pure_sensitivity, extra_fn=None, ), + FILTER_LAST_RESET_DESCRIPTION, ) DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( @@ -133,6 +143,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( value_fn=lambda data: data.timer_time, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), + FILTER_LAST_RESET_DESCRIPTION, ) diff --git a/tests/components/sensibo/fixtures/data.json b/tests/components/sensibo/fixtures/data.json index 6c44b44821f..5837296d154 100644 --- a/tests/components/sensibo/fixtures/data.json +++ b/tests/components/sensibo/fixtures/data.json @@ -156,7 +156,7 @@ "time": "2022-03-12T15:24:26Z", "secondsAgo": 4219143 }, - "shouldCleanFilters": false + "shouldCleanFilters": true }, "serviceSubscriptions": [], "roomIsOccupied": true, diff --git a/tests/components/sensibo/test_button.py b/tests/components/sensibo/test_button.py new file mode 100644 index 00000000000..66b7a1258b1 --- /dev/null +++ b/tests/components/sensibo/test_button.py @@ -0,0 +1,110 @@ +"""The test for the sensibo button platform.""" +from __future__ import annotations + +from datetime import datetime, timedelta +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +from pysensibo.model import SensiboData +from pytest import MonkeyPatch, raises + +from homeassistant.components.button import SERVICE_PRESS +from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def test_button( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Sensibo button.""" + + state_button = hass.states.get("button.hallway_reset_filter") + state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required") + state_filter_last_reset = hass.states.get("sensor.hallway_filter_last_reset") + + assert state_button.state is STATE_UNKNOWN + assert state_filter_clean.state is STATE_ON + assert state_filter_last_reset.state == "2022-03-12T15:24:26+00:00" + + freezer.move_to(datetime(2022, 6, 19, 20, 0, 0)) + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", + return_value={"status": "success"}, + ): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: state_button.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["ABC999111"], "filter_clean", False) + monkeypatch.setattr( + get_data.parsed["ABC999111"], + "filter_last_reset", + datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC), + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state_button = hass.states.get("button.hallway_reset_filter") + state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required") + state_filter_last_reset = hass.states.get("sensor.hallway_filter_last_reset") + assert ( + state_button.state == datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC).isoformat() + ) + assert state_filter_clean.state is STATE_OFF + assert state_filter_last_reset.state == "2022-06-19T20:00:00+00:00" + + +async def test_button_failure( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo button fails.""" + + state_button = hass.states.get("button.hallway_reset_filter") + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", + return_value={"status": "failure"}, + ): + with raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: state_button.entity_id, + }, + blocking=True, + )