Add battery sensor for gogogate2 wireless door sensor (#47145)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Erik Montnemery 2021-03-02 04:34:37 +01:00 committed by GitHub
parent 853d9ac4a9
commit 7e71050669
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 388 additions and 46 deletions

View File

@ -1,5 +1,8 @@
"""The gogogate2 component."""
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
import asyncio
from homeassistant.components.cover import DOMAIN as COVER
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE
from homeassistant.core import HomeAssistant
@ -8,6 +11,8 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .common import get_data_update_coordinator
from .const import DEVICE_TYPE_GOGOGATE2
PLATFORMS = [COVER, SENSOR]
async def async_setup(hass: HomeAssistant, base_config: dict) -> bool:
"""Set up for Gogogate2 controllers."""
@ -34,17 +39,23 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
if not data_update_coordinator.last_update_success:
raise ConfigEntryNotReady()
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, COVER_DOMAIN)
)
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Gogogate2 config entry."""
hass.async_create_task(
hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN)
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, platform)
for platform in PLATFORMS
]
)
)
return True
return unload_ok

View File

@ -4,7 +4,7 @@ import logging
from typing import Awaitable, Callable, NamedTuple, Optional
from gogogate2_api import AbstractGateApi, GogoGate2Api, ISmartGateApi
from gogogate2_api.common import AbstractDoor
from gogogate2_api.common import AbstractDoor, get_door_by_id
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@ -15,9 +15,13 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import DATA_UPDATE_COORDINATOR, DEVICE_TYPE_ISMARTGATE, DOMAIN
from .const import DATA_UPDATE_COORDINATOR, DEVICE_TYPE_ISMARTGATE, DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
@ -57,6 +61,44 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
self.api = api
class GoGoGate2Entity(CoordinatorEntity):
"""Base class for gogogate2 entities."""
def __init__(
self,
config_entry: ConfigEntry,
data_update_coordinator: DeviceDataUpdateCoordinator,
door: AbstractDoor,
) -> 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)
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return self._unique_id
def _get_door(self) -> AbstractDoor:
door = get_door_by_id(self._door.door_id, self.coordinator.data)
self._door = door or self._door
return self._door
@property
def device_info(self):
"""Device info for the controller."""
data = self.coordinator.data
return {
"identifiers": {(DOMAIN, self._config_entry.unique_id)},
"name": self._config_entry.title,
"manufacturer": MANUFACTURER,
"model": data.model,
"sw_version": data.firmwareversion,
}
def get_data_update_coordinator(
hass: HomeAssistant, config_entry: ConfigEntry
) -> DeviceDataUpdateCoordinator:

View File

@ -1,12 +1,7 @@
"""Support for Gogogate2 garage Doors."""
from typing import Callable, List, Optional
from gogogate2_api.common import (
AbstractDoor,
DoorStatus,
get_configured_doors,
get_door_by_id,
)
from gogogate2_api.common import AbstractDoor, DoorStatus, get_configured_doors
import voluptuous as vol
from homeassistant.components.cover import (
@ -26,14 +21,13 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .common import (
DeviceDataUpdateCoordinator,
cover_unique_id,
GoGoGate2Entity,
get_data_update_coordinator,
)
from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN, MANUFACTURER
from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN
COVER_SCHEMA = vol.Schema(
{
@ -74,7 +68,7 @@ async def async_setup_entry(
)
class DeviceCover(CoordinatorEntity, CoverEntity):
class DeviceCover(GoGoGate2Entity, CoverEntity):
"""Cover entity for goggate2."""
def __init__(
@ -84,18 +78,10 @@ class DeviceCover(CoordinatorEntity, CoverEntity):
door: AbstractDoor,
) -> None:
"""Initialize the object."""
super().__init__(data_update_coordinator)
self._config_entry = config_entry
self._door = door
super().__init__(config_entry, data_update_coordinator, door)
self._api = data_update_coordinator.api
self._unique_id = cover_unique_id(config_entry, door)
self._is_available = True
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return self._unique_id
@property
def name(self):
"""Return the name of the door."""
@ -141,20 +127,3 @@ class DeviceCover(CoordinatorEntity, CoverEntity):
attrs = super().state_attributes
attrs["door_id"] = self._get_door().door_id
return attrs
def _get_door(self) -> AbstractDoor:
door = get_door_by_id(self._door.door_id, self.coordinator.data)
self._door = door or self._door
return self._door
@property
def device_info(self):
"""Device info for the controller."""
data = self.coordinator.data
return {
"identifiers": {(DOMAIN, self._config_entry.unique_id)},
"name": self._config_entry.title,
"manufacturer": MANUFACTURER,
"model": data.model,
"sw_version": data.firmwareversion,
}

View File

@ -0,0 +1,59 @@
"""Support for Gogogate2 garage Doors."""
from typing import Callable, List, Optional
from gogogate2_api.common import get_configured_doors
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DEVICE_CLASS_BATTERY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from .common import GoGoGate2Entity, get_data_update_coordinator
SENSOR_ID_WIRED = "WIRE"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: Callable[[List[Entity], Optional[bool]], None],
) -> None:
"""Set up the config entry."""
data_update_coordinator = get_data_update_coordinator(hass, config_entry)
async_add_entities(
[
DoorSensor(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
]
)
class DoorSensor(GoGoGate2Entity):
"""Sensor entity for goggate2."""
@property
def name(self):
"""Return the name of the door."""
return f"{self._get_door().name} battery"
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return DEVICE_CLASS_BATTERY
@property
def state(self):
"""Return the state of the entity."""
door = self._get_door()
return door.voltage # This is a percentage, not an absolute voltage
@property
def state_attributes(self):
"""Return the state attributes."""
attrs = super().state_attributes or {}
door = self._get_door()
if door.sensorid is not None:
attrs["sensorid"] = door.door_id
return attrs

View File

@ -0,0 +1,261 @@
"""Tests for the GogoGate2 component."""
from datetime import timedelta
from unittest.mock import MagicMock, patch
from gogogate2_api import GogoGate2Api, ISmartGateApi
from gogogate2_api.common import (
DoorMode,
DoorStatus,
GogoGate2ActivateResponse,
GogoGate2Door,
GogoGate2InfoResponse,
ISmartGateDoor,
ISmartGateInfoResponse,
Network,
Outputs,
Wifi,
)
from homeassistant.components.gogogate2.const import DEVICE_TYPE_ISMARTGATE, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
ATTR_DEVICE_CLASS,
CONF_DEVICE,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_USERNAME,
DEVICE_CLASS_BATTERY,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow
from tests.common import MockConfigEntry, async_fire_time_changed
def _mocked_gogogate_sensor_response(battery_level: int):
return GogoGate2InfoResponse(
user="user1",
gogogatename="gogogatename0",
model="",
apiversion="",
remoteaccessenabled=False,
remoteaccess="abc123.blah.blah",
firmwareversion="",
apicode="",
door1=GogoGate2Door(
door_id=1,
permission=True,
name="Door1",
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.OPENED,
sensor=True,
sensorid="ABCD",
camera=False,
events=2,
temperature=None,
voltage=battery_level,
),
door2=GogoGate2Door(
door_id=2,
permission=True,
name="Door2",
gate=True,
mode=DoorMode.GARAGE,
status=DoorStatus.UNDEFINED,
sensor=True,
sensorid="WIRE",
camera=False,
events=0,
temperature=None,
voltage=battery_level,
),
door3=GogoGate2Door(
door_id=3,
permission=True,
name="Door3",
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.UNDEFINED,
sensor=True,
sensorid=None,
camera=False,
events=0,
temperature=None,
voltage=battery_level,
),
outputs=Outputs(output1=True, output2=False, output3=True),
network=Network(ip=""),
wifi=Wifi(SSID="", linkquality="", signal=""),
)
def _mocked_ismartgate_sensor_response(battery_level: int):
return ISmartGateInfoResponse(
user="user1",
ismartgatename="ismartgatename0",
model="ismartgatePRO",
apiversion="",
remoteaccessenabled=False,
remoteaccess="abc321.blah.blah",
firmwareversion="555",
pin=123,
lang="en",
newfirmware=False,
door1=ISmartGateDoor(
door_id=1,
permission=True,
name="Door1",
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.CLOSED,
sensor=True,
sensorid="ABCD",
camera=False,
events=2,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
voltage=battery_level,
),
door2=ISmartGateDoor(
door_id=2,
permission=True,
name="Door2",
gate=True,
mode=DoorMode.GARAGE,
status=DoorStatus.CLOSED,
sensor=True,
sensorid="WIRE",
camera=False,
events=2,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
voltage=battery_level,
),
door3=ISmartGateDoor(
door_id=3,
permission=True,
name="Door3",
gate=False,
mode=DoorMode.GARAGE,
status=DoorStatus.UNDEFINED,
sensor=True,
sensorid=None,
camera=False,
events=0,
temperature=None,
enabled=True,
apicode="apicode0",
customimage=False,
voltage=battery_level,
),
network=Network(ip=""),
wifi=Wifi(SSID="", linkquality="", signal=""),
)
@patch("homeassistant.components.gogogate2.common.GogoGate2Api")
async def test_sensor_update(gogogate2api_mock, hass: HomeAssistant) -> None:
"""Test data update."""
api = MagicMock(GogoGate2Api)
api.async_activate.return_value = GogoGate2ActivateResponse(result=True)
api.async_info.return_value = _mocked_gogogate_sensor_response(25)
gogogate2api_mock.return_value = api
config_entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_USER,
data={
CONF_IP_ADDRESS: "127.0.0.1",
CONF_USERNAME: "admin",
CONF_PASSWORD: "password",
},
)
config_entry.add_to_hass(hass)
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("sensor.door1_battery") is None
assert hass.states.get("sensor.door2_battery") is None
assert hass.states.get("sensor.door2_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("sensor.door1_battery").state == "25"
assert hass.states.get("sensor.door2_battery") is None
assert hass.states.get("sensor.door2_battery") is None
api.async_info.return_value = _mocked_gogogate_sensor_response(40)
async_fire_time_changed(hass, utcnow() + timedelta(hours=2))
await hass.async_block_till_done()
assert hass.states.get("sensor.door1_battery").state == "40"
api.async_info.return_value = _mocked_gogogate_sensor_response(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 await hass.config_entries.async_unload(config_entry.entry_id)
assert not hass.states.async_entity_ids(DOMAIN)
@patch("homeassistant.components.gogogate2.common.ISmartGateApi")
async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None:
"""Test availability."""
sensor_response = _mocked_ismartgate_sensor_response(35)
api = MagicMock(ISmartGateApi)
api.async_info.return_value = sensor_response
ismartgateapi_mock.return_value = api
config_entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_USER,
data={
CONF_DEVICE: DEVICE_TYPE_ISMARTGATE,
CONF_IP_ADDRESS: "127.0.0.1",
CONF_USERNAME: "admin",
CONF_PASSWORD: "password",
},
)
config_entry.add_to_hass(hass)
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("sensor.door1_battery") is None
assert hass.states.get("sensor.door2_battery") is None
assert hass.states.get("sensor.door2_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("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.door1_battery").attributes[ATTR_DEVICE_CLASS]
== DEVICE_CLASS_BATTERY
)
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
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"