From 7aaa08f153d4814e99a9cda9fdc36f2758170d44 Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Tue, 13 Jul 2021 18:37:09 +0200 Subject: [PATCH] Add binary_sensor support to Freedompro (#52717) * Update Freedompro * change _attr_unique_id with unique_id --- .../components/freedompro/__init__.py | 2 +- .../components/freedompro/binary_sensor.py | 86 +++++++++++++ tests/components/freedompro/const.py | 2 +- .../freedompro/test_binary_sensor.py | 114 ++++++++++++++++++ 4 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/freedompro/binary_sensor.py create mode 100644 tests/components/freedompro/test_binary_sensor.py diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index ddb6688a127..6b377f51560 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py new file mode 100644 index 00000000000..b629f4c0af4 --- /dev/null +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -0,0 +1,86 @@ +"""Support for Freedompro binary_sensor.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_SMOKE, + BinarySensorEntity, +) +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + +DEVICE_CLASS_MAP = { + "smokeSensor": DEVICE_CLASS_SMOKE, + "occupancySensor": DEVICE_CLASS_OCCUPANCY, + "motionSensor": DEVICE_CLASS_MOTION, + "contactSensor": DEVICE_CLASS_OPENING, +} + +DEVICE_KEY_MAP = { + "smokeSensor": "smokeDetected", + "occupancySensor": "occupancyDetected", + "motionSensor": "motionDetected", + "contactSensor": "contactSensorState", +} + +SUPPORTED_SENSORS = {"smokeSensor", "occupancySensor", "motionSensor", "contactSensor"} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro binary_sensor.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] in SUPPORTED_SENSORS + ) + + +class Device(CoordinatorEntity, BinarySensorEntity): + """Representation of an Freedompro binary_sensor.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro binary_sensor.""" + super().__init__(coordinator) + self._hass = hass + self._session = aiohttp_client.async_get_clientsession(self._hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._type = device["type"] + self._characteristics = device["characteristics"] + self._attr_device_info = { + "name": self.name, + "identifiers": { + (DOMAIN, self.unique_id), + }, + "model": self._type, + "manufacturer": "Freedompro", + } + self._attr_device_class = DEVICE_CLASS_MAP[self._type] + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self.unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + self._attr_is_on = state[DEVICE_KEY_MAP[self._type]] + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/tests/components/freedompro/const.py b/tests/components/freedompro/const.py index 0db9d5884b1..2f59d190e2c 100644 --- a/tests/components/freedompro/const.py +++ b/tests/components/freedompro/const.py @@ -131,7 +131,7 @@ DEVICES_STATE = [ { "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C", "type": "contactSensor", - "state": {"contactSensorState": True}, + "state": {"contactSensorState": False}, "online": True, }, { diff --git a/tests/components/freedompro/test_binary_sensor.py b/tests/components/freedompro/test_binary_sensor.py new file mode 100644 index 00000000000..785a6b03212 --- /dev/null +++ b/tests/components/freedompro/test_binary_sensor.py @@ -0,0 +1,114 @@ +"""Tests for the Freedompro binary sensor.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.freedompro.const import DEVICES_STATE + + +@pytest.mark.parametrize( + "entity_id, uid, name, model", + [ + ( + "binary_sensor.doorway_motion_sensor", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*VTEPEDYE8DXGS8U94CJKQDLKMN6CUX1IJWSOER2HZCK", + "Doorway motion sensor", + "motionSensor", + ), + ( + "binary_sensor.contact_sensor_living_room", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C", + "Contact sensor living room", + "contactSensor", + ), + ( + "binary_sensor.living_room_occupancy_sensor", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SNG7Y3R1R0S_W5BCNPP1O5WUN2NCEOOT27EFSYT6JYS", + "Living room occupancy sensor", + "occupancySensor", + ), + ( + "binary_sensor.smoke_sensor_kitchen", + "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SXFMEXI4UMDBAMXXPI6LJV47O9NY-IRCAKZI7_MW0LY", + "Smoke sensor kitchen", + "smokeSensor", + ), + ], +) +async def test_binary_sensor_get_state( + hass, init_integration, entity_id: str, uid: str, name: str, model: str +): + """Test states of the binary_sensor.""" + init_integration + registry = er.async_get(hass) + registry_device = dr.async_get(hass) + + device = registry_device.async_get_device({("freedompro", uid)}) + assert device is not None + assert device.identifiers == {("freedompro", uid)} + assert device.manufacturer == "Freedompro" + assert device.name == name + assert device.model == model + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_OFF + + with patch( + "homeassistant.components.freedompro.get_states", + return_value=[], + ): + + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_OFF + + get_states_response = list(DEVICES_STATE) + for state_response in get_states_response: + if state_response["uid"] == uid: + if state_response["type"] == "smokeSensor": + state_response["state"]["smokeDetected"] = True + if state_response["type"] == "occupancySensor": + state_response["state"]["occupancyDetected"] = True + if state_response["type"] == "motionSensor": + state_response["state"]["motionDetected"] = True + if state_response["type"] == "contactSensor": + state_response["state"]["contactSensorState"] = True + with patch( + "homeassistant.components.freedompro.get_states", + return_value=get_states_response, + ): + + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("friendly_name") == name + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == uid + + assert state.state == STATE_ON