From 3115bf9aaba476b69b82c75fab0ac1930ff31d26 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 12 Mar 2021 19:04:56 +0100 Subject: [PATCH] Add temperature sensor for gogogate2 wireless door sensor (#47754) * Add temperature sensor for gogogate2 wireless door sensor * Chain sensor generators --- homeassistant/components/gogogate2/common.py | 10 ++- homeassistant/components/gogogate2/cover.py | 4 +- homeassistant/components/gogogate2/sensor.py | 85 +++++++++++++++++-- tests/components/gogogate2/test_sensor.py | 88 ++++++++++++++------ 4 files changed, 152 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 761f9211921..404a22e3312 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -69,12 +69,13 @@ class GoGoGate2Entity(CoordinatorEntity): config_entry: ConfigEntry, data_update_coordinator: DeviceDataUpdateCoordinator, door: AbstractDoor, + unique_id: str, ) -> None: """Initialize gogogate2 base entity.""" super().__init__(data_update_coordinator) self._config_entry = config_entry self._door = door - self._unique_id = cover_unique_id(config_entry, door) + self._unique_id = unique_id @property def unique_id(self) -> Optional[str]: @@ -137,6 +138,13 @@ def cover_unique_id(config_entry: ConfigEntry, door: AbstractDoor) -> str: return f"{config_entry.unique_id}_{door.door_id}" +def sensor_unique_id( + config_entry: ConfigEntry, door: AbstractDoor, sensor_type: str +) -> str: + """Generate a cover entity unique id.""" + return f"{config_entry.unique_id}_{door.door_id}_{sensor_type}" + + def get_api(config_data: dict) -> AbstractGateApi: """Get an api object for config data.""" gate_class = GogoGate2Api diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 2d97edbfe43..9a1b53f7bee 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity import Entity from .common import ( DeviceDataUpdateCoordinator, GoGoGate2Entity, + cover_unique_id, get_data_update_coordinator, ) from .const import DOMAIN @@ -66,7 +67,8 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): door: AbstractDoor, ) -> None: """Initialize the object.""" - super().__init__(config_entry, data_update_coordinator, door) + unique_id = cover_unique_id(config_entry, door) + super().__init__(config_entry, data_update_coordinator, door, unique_id) self._api = data_update_coordinator.api self._is_available = True diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index ed53779a95a..eea557639ad 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -1,14 +1,24 @@ """Support for Gogogate2 garage Doors.""" +from itertools import chain from typing import Callable, List, Optional -from gogogate2_api.common import get_configured_doors +from gogogate2_api.common import AbstractDoor, get_configured_doors from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_BATTERY +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .common import GoGoGate2Entity, get_data_update_coordinator +from .common import ( + DeviceDataUpdateCoordinator, + GoGoGate2Entity, + get_data_update_coordinator, + sensor_unique_id, +) SENSOR_ID_WIRED = "WIRE" @@ -21,17 +31,33 @@ async def async_setup_entry( """Set up the config entry.""" data_update_coordinator = get_data_update_coordinator(hass, config_entry) - async_add_entities( + sensors = chain( [ - DoorSensor(config_entry, data_update_coordinator, door) + DoorSensorBattery(config_entry, data_update_coordinator, door) for door in get_configured_doors(data_update_coordinator.data) if door.sensorid and door.sensorid != SENSOR_ID_WIRED - ] + ], + [ + DoorSensorTemperature(config_entry, data_update_coordinator, door) + for door in get_configured_doors(data_update_coordinator.data) + if door.sensorid and door.sensorid != SENSOR_ID_WIRED + ], ) + async_add_entities(sensors) -class DoorSensor(GoGoGate2Entity): - """Sensor entity for goggate2.""" +class DoorSensorBattery(GoGoGate2Entity): + """Battery sensor entity for gogogate2 door sensor.""" + + def __init__( + self, + config_entry: ConfigEntry, + data_update_coordinator: DeviceDataUpdateCoordinator, + door: AbstractDoor, + ) -> None: + """Initialize the object.""" + unique_id = sensor_unique_id(config_entry, door, "battery") + super().__init__(config_entry, data_update_coordinator, door, unique_id) @property def name(self): @@ -56,3 +82,46 @@ class DoorSensor(GoGoGate2Entity): if door.sensorid is not None: return {"door_id": door.door_id, "sensor_id": door.sensorid} return None + + +class DoorSensorTemperature(GoGoGate2Entity): + """Temperature sensor entity for gogogate2 door sensor.""" + + def __init__( + self, + config_entry: ConfigEntry, + data_update_coordinator: DeviceDataUpdateCoordinator, + door: AbstractDoor, + ) -> None: + """Initialize the object.""" + unique_id = sensor_unique_id(config_entry, door, "temperature") + super().__init__(config_entry, data_update_coordinator, door, unique_id) + + @property + def name(self): + """Return the name of the door.""" + return f"{self._get_door().name} temperature" + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def state(self): + """Return the state of the entity.""" + door = self._get_door() + return door.temperature + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes.""" + door = self._get_door() + if door.sensorid is not None: + return {"door_id": door.door_id, "sensor_id": door.sensorid} + return None diff --git a/tests/components/gogogate2/test_sensor.py b/tests/components/gogogate2/test_sensor.py index f8eb9bf88b7..020989c003a 100644 --- a/tests/components/gogogate2/test_sensor.py +++ b/tests/components/gogogate2/test_sensor.py @@ -20,11 +20,13 @@ from homeassistant.components.gogogate2.const import DEVICE_TYPE_ISMARTGATE, DOM from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICE, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -34,7 +36,7 @@ from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed -def _mocked_gogogate_sensor_response(battery_level: int): +def _mocked_gogogate_sensor_response(battery_level: int, temperature: float): return GogoGate2InfoResponse( user="user1", gogogatename="gogogatename0", @@ -55,7 +57,7 @@ def _mocked_gogogate_sensor_response(battery_level: int): sensorid="ABCD", camera=False, events=2, - temperature=None, + temperature=temperature, voltage=battery_level, ), door2=GogoGate2Door( @@ -69,7 +71,7 @@ def _mocked_gogogate_sensor_response(battery_level: int): sensorid="WIRE", camera=False, events=0, - temperature=None, + temperature=temperature, voltage=battery_level, ), door3=GogoGate2Door( @@ -83,7 +85,7 @@ def _mocked_gogogate_sensor_response(battery_level: int): sensorid=None, camera=False, events=0, - temperature=None, + temperature=temperature, voltage=battery_level, ), outputs=Outputs(output1=True, output2=False, output3=True), @@ -92,7 +94,7 @@ def _mocked_gogogate_sensor_response(battery_level: int): ) -def _mocked_ismartgate_sensor_response(battery_level: int): +def _mocked_ismartgate_sensor_response(battery_level: int, temperature: float): return ISmartGateInfoResponse( user="user1", ismartgatename="ismartgatename0", @@ -115,7 +117,7 @@ def _mocked_ismartgate_sensor_response(battery_level: int): sensorid="ABCD", camera=False, events=2, - temperature=None, + temperature=temperature, enabled=True, apicode="apicode0", customimage=False, @@ -132,7 +134,7 @@ def _mocked_ismartgate_sensor_response(battery_level: int): sensorid="WIRE", camera=False, events=2, - temperature=None, + temperature=temperature, enabled=True, apicode="apicode0", customimage=False, @@ -149,7 +151,7 @@ def _mocked_ismartgate_sensor_response(battery_level: int): sensorid=None, camera=False, events=0, - temperature=None, + temperature=temperature, enabled=True, apicode="apicode0", customimage=False, @@ -164,16 +166,23 @@ def _mocked_ismartgate_sensor_response(battery_level: int): async def test_sensor_update(gogogate2api_mock, hass: HomeAssistant) -> None: """Test data update.""" - expected_attributes = { + bat_attributes = { "device_class": "battery", "door_id": 1, "friendly_name": "Door1 battery", "sensor_id": "ABCD", } + temp_attributes = { + "device_class": "temperature", + "door_id": 1, + "friendly_name": "Door1 temperature", + "sensor_id": "ABCD", + "unit_of_measurement": "°C", + } api = MagicMock(GogoGate2Api) api.async_activate.return_value = GogoGate2ActivateResponse(result=True) - api.async_info.return_value = _mocked_gogogate_sensor_response(25) + api.async_info.return_value = _mocked_gogogate_sensor_response(25, 7.0) gogogate2api_mock.return_value = api config_entry = MockConfigEntry( @@ -189,31 +198,40 @@ async def test_sensor_update(gogogate2api_mock, hass: HomeAssistant) -> None: assert hass.states.get("cover.door1") is None assert hass.states.get("cover.door2") is None - assert hass.states.get("cover.door2") is None + assert hass.states.get("cover.door3") is None assert hass.states.get("sensor.door1_battery") is None assert hass.states.get("sensor.door2_battery") is None - assert hass.states.get("sensor.door2_battery") is None + assert hass.states.get("sensor.door3_battery") is None + assert hass.states.get("sensor.door1_temperature") is None + assert hass.states.get("sensor.door2_temperature") is None + assert hass.states.get("sensor.door3_temperature") is None assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get("cover.door1") assert hass.states.get("cover.door2") - assert hass.states.get("cover.door2") + assert hass.states.get("cover.door3") assert hass.states.get("sensor.door1_battery").state == "25" + assert dict(hass.states.get("sensor.door1_battery").attributes) == bat_attributes + assert hass.states.get("sensor.door2_battery") is None + assert hass.states.get("sensor.door2_battery") is None + assert hass.states.get("sensor.door1_temperature").state == "7.0" assert ( - dict(hass.states.get("sensor.door1_battery").attributes) == expected_attributes + dict(hass.states.get("sensor.door1_temperature").attributes) == temp_attributes ) - assert hass.states.get("sensor.door2_battery") is None - assert hass.states.get("sensor.door2_battery") is None + assert hass.states.get("sensor.door2_temperature") is None + assert hass.states.get("sensor.door3_temperature") is None - api.async_info.return_value = _mocked_gogogate_sensor_response(40) + api.async_info.return_value = _mocked_gogogate_sensor_response(40, 10.0) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("sensor.door1_battery").state == "40" + assert hass.states.get("sensor.door1_temperature").state == "10.0" - api.async_info.return_value = _mocked_gogogate_sensor_response(None) + api.async_info.return_value = _mocked_gogogate_sensor_response(None, None) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("sensor.door1_battery").state == STATE_UNKNOWN + assert hass.states.get("sensor.door1_temperature").state == STATE_UNKNOWN assert await hass.config_entries.async_unload(config_entry.entry_id) assert not hass.states.async_entity_ids(DOMAIN) @@ -222,14 +240,21 @@ async def test_sensor_update(gogogate2api_mock, hass: HomeAssistant) -> None: @patch("homeassistant.components.gogogate2.common.ISmartGateApi") async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: """Test availability.""" - expected_attributes = { + bat_attributes = { "device_class": "battery", "door_id": 1, "friendly_name": "Door1 battery", "sensor_id": "ABCD", } + temp_attributes = { + "device_class": "temperature", + "door_id": 1, + "friendly_name": "Door1 temperature", + "sensor_id": "ABCD", + "unit_of_measurement": "°C", + } - sensor_response = _mocked_ismartgate_sensor_response(35) + sensor_response = _mocked_ismartgate_sensor_response(35, -4.0) api = MagicMock(ISmartGateApi) api.async_info.return_value = sensor_response ismartgateapi_mock.return_value = api @@ -248,34 +273,47 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: assert hass.states.get("cover.door1") is None assert hass.states.get("cover.door2") is None - assert hass.states.get("cover.door2") is None + assert hass.states.get("cover.door3") is None assert hass.states.get("sensor.door1_battery") is None assert hass.states.get("sensor.door2_battery") is None - assert hass.states.get("sensor.door2_battery") is None + assert hass.states.get("sensor.door3_battery") is None assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get("cover.door1") assert hass.states.get("cover.door2") - assert hass.states.get("cover.door2") + assert hass.states.get("cover.door3") assert hass.states.get("sensor.door1_battery").state == "35" assert hass.states.get("sensor.door2_battery") is None - assert hass.states.get("sensor.door2_battery") is None + assert hass.states.get("sensor.door3_battery") is None + assert hass.states.get("sensor.door1_temperature").state == "-4.0" + assert hass.states.get("sensor.door2_temperature") is None + assert hass.states.get("sensor.door3_temperature") is None assert ( hass.states.get("sensor.door1_battery").attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_BATTERY ) + assert ( + hass.states.get("sensor.door1_temperature").attributes[ATTR_DEVICE_CLASS] + == DEVICE_CLASS_TEMPERATURE + ) + assert ( + hass.states.get("sensor.door1_temperature").attributes[ATTR_UNIT_OF_MEASUREMENT] + == "°C" + ) api.async_info.side_effect = Exception("Error") async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("sensor.door1_battery").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.door1_temperature").state == STATE_UNAVAILABLE api.async_info.side_effect = None api.async_info.return_value = sensor_response async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("sensor.door1_battery").state == "35" + assert dict(hass.states.get("sensor.door1_battery").attributes) == bat_attributes assert ( - dict(hass.states.get("sensor.door1_battery").attributes) == expected_attributes + dict(hass.states.get("sensor.door1_temperature").attributes) == temp_attributes )