mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add battery sensor for gogogate2 wireless door sensor (#47145)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
853d9ac4a9
commit
7e71050669
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
}
|
||||
|
59
homeassistant/components/gogogate2/sensor.py
Normal file
59
homeassistant/components/gogogate2/sensor.py
Normal 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
|
261
tests/components/gogogate2/test_sensor.py
Normal file
261
tests/components/gogogate2/test_sensor.py
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user