Add diagnostic sensors for TotalConnect (#73152)
* add diagnostic sensors * test binary_sensor.py file * add tests for binary sensor * fix zone type checks and error on unknown * improve entity tests * hide entities by default * Revert "hide entities by default" This reverts commit 9808d732471385e45ccc5f7c3aea93bfecbdfa6f. * Update homeassistant/components/totalconnect/binary_sensor.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * update binary_sensor per comments * update test * move to _attr_extra_state_attributes * no spaces in unique_id * update per balloob suggestions * fix typing * fix black and mypy * Apply suggestions from code review Co-authored-by: G Johansson <goran.johansson@shiftit.se> * add more to binary_sensor tests * remove unused import --------- Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: G Johansson <goran.johansson@shiftit.se>pull/92738/head
parent
bf6d429339
commit
16c915864b
|
@ -1293,7 +1293,6 @@ omit =
|
|||
homeassistant/components/toon/switch.py
|
||||
homeassistant/components/torque/sensor.py
|
||||
homeassistant/components/totalconnect/__init__.py
|
||||
homeassistant/components/totalconnect/binary_sensor.py
|
||||
homeassistant/components/touchline/climate.py
|
||||
homeassistant/components/tplink_lte/*
|
||||
homeassistant/components/tplink_omada/__init__.py
|
||||
|
|
|
@ -1,74 +1,78 @@
|
|||
"""Interfaces with TotalConnect sensors."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
LOW_BATTERY = "low_battery"
|
||||
TAMPER = "tamper"
|
||||
POWER = "power"
|
||||
ZONE = "zone"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up TotalConnect device sensors based on a config entry."""
|
||||
sensors = []
|
||||
sensors: list = []
|
||||
|
||||
client_locations = hass.data[DOMAIN][entry.entry_id].client.locations
|
||||
|
||||
for location_id, location in client_locations.items():
|
||||
for zone_id, zone in location.zones.items():
|
||||
sensors.append(TotalConnectBinarySensor(zone_id, location_id, zone))
|
||||
sensors.append(TotalConnectAlarmLowBatteryBinarySensor(location))
|
||||
sensors.append(TotalConnectAlarmTamperBinarySensor(location))
|
||||
sensors.append(TotalConnectAlarmPowerBinarySensor(location))
|
||||
|
||||
for zone in location.zones.values():
|
||||
sensors.append(TotalConnectZoneSecurityBinarySensor(location_id, zone))
|
||||
|
||||
if not zone.is_type_button():
|
||||
sensors.append(TotalConnectLowBatteryBinarySensor(location_id, zone))
|
||||
sensors.append(TotalConnectTamperBinarySensor(location_id, zone))
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class TotalConnectBinarySensor(BinarySensorEntity):
|
||||
class TotalConnectZoneBinarySensor(BinarySensorEntity):
|
||||
"""Represent an TotalConnect zone."""
|
||||
|
||||
def __init__(self, zone_id, location_id, zone):
|
||||
def __init__(self, location_id, zone):
|
||||
"""Initialize the TotalConnect status."""
|
||||
self._zone_id = zone_id
|
||||
self._location_id = location_id
|
||||
self._zone = zone
|
||||
self._name = self._zone.description
|
||||
self._unique_id = f"{location_id} {zone_id}"
|
||||
self._is_on = None
|
||||
self._is_tampered = None
|
||||
self._is_low_battery = None
|
||||
self._attr_name = f"{zone.description}{self.entity_description.name}"
|
||||
self._attr_unique_id = (
|
||||
f"{location_id}_{zone.zoneid}_{self.entity_description.key}"
|
||||
)
|
||||
self._attr_is_on = None
|
||||
self._attr_extra_state_attributes = {
|
||||
"zone_id": self._zone.zoneid,
|
||||
"location_id": self._location_id,
|
||||
"partition": self._zone.partition,
|
||||
}
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
class TotalConnectZoneSecurityBinarySensor(TotalConnectZoneBinarySensor):
|
||||
"""Represent an TotalConnect security zone."""
|
||||
|
||||
def update(self) -> None:
|
||||
"""Return the state of the device."""
|
||||
self._is_tampered = self._zone.is_tampered()
|
||||
self._is_low_battery = self._zone.is_low_battery()
|
||||
|
||||
if self._zone.is_faulted() or self._zone.is_triggered():
|
||||
self._is_on = True
|
||||
else:
|
||||
self._is_on = False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._is_on
|
||||
entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription(
|
||||
key=ZONE, name=""
|
||||
)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from BinarySensorDeviceClass."""
|
||||
if self._zone.is_type_security():
|
||||
return BinarySensorDeviceClass.DOOR
|
||||
"""Return the class of this zone."""
|
||||
if self._zone.is_type_fire():
|
||||
return BinarySensorDeviceClass.SMOKE
|
||||
if self._zone.is_type_carbon_monoxide():
|
||||
|
@ -77,16 +81,108 @@ class TotalConnectBinarySensor(BinarySensorEntity):
|
|||
return BinarySensorDeviceClass.MOTION
|
||||
if self._zone.is_type_medical():
|
||||
return BinarySensorDeviceClass.SAFETY
|
||||
# "security" type is a generic category so test for it last
|
||||
if self._zone.is_type_security():
|
||||
return BinarySensorDeviceClass.DOOR
|
||||
|
||||
_LOGGER.error(
|
||||
"TotalConnect zone %s reported an unexpected device class",
|
||||
self._zone.zoneid,
|
||||
)
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attributes = {
|
||||
"zone_id": self._zone_id,
|
||||
"location_id": self._location_id,
|
||||
"low_battery": self._is_low_battery,
|
||||
"tampered": self._is_tampered,
|
||||
"partition": self._zone.partition,
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
if self._zone.is_faulted() or self._zone.is_triggered():
|
||||
self._attr_is_on = True
|
||||
else:
|
||||
self._attr_is_on = False
|
||||
|
||||
|
||||
class TotalConnectLowBatteryBinarySensor(TotalConnectZoneBinarySensor):
|
||||
"""Represent an TotalConnect zone low battery status."""
|
||||
|
||||
entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription(
|
||||
key=LOW_BATTERY,
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
name=" low battery",
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
self._attr_is_on = self._zone.is_low_battery()
|
||||
|
||||
|
||||
class TotalConnectTamperBinarySensor(TotalConnectZoneBinarySensor):
|
||||
"""Represent an TotalConnect zone tamper status."""
|
||||
|
||||
entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription(
|
||||
key=TAMPER,
|
||||
device_class=BinarySensorDeviceClass.TAMPER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
name=f" {TAMPER}",
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
self._attr_is_on = self._zone.is_tampered()
|
||||
|
||||
|
||||
class TotalConnectAlarmBinarySensor(BinarySensorEntity):
|
||||
"""Represent an TotalConnect alarm device binary sensors."""
|
||||
|
||||
def __init__(self, location):
|
||||
"""Initialize the TotalConnect alarm device binary sensor."""
|
||||
self._location = location
|
||||
self._attr_name = f"{location.location_name}{self.entity_description.name}"
|
||||
self._attr_unique_id = f"{location.location_id}_{self.entity_description.key}"
|
||||
self._attr_is_on = None
|
||||
self._attr_extra_state_attributes = {
|
||||
"location_id": self._location.location_id,
|
||||
}
|
||||
return attributes
|
||||
|
||||
|
||||
class TotalConnectAlarmLowBatteryBinarySensor(TotalConnectAlarmBinarySensor):
|
||||
"""Represent an TotalConnect Alarm low battery status."""
|
||||
|
||||
entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription(
|
||||
key=LOW_BATTERY,
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
name=" low battery",
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
self._attr_is_on = self._location.is_low_battery()
|
||||
|
||||
|
||||
class TotalConnectAlarmTamperBinarySensor(TotalConnectAlarmBinarySensor):
|
||||
"""Represent an TotalConnect alarm tamper status."""
|
||||
|
||||
entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription(
|
||||
key=TAMPER,
|
||||
device_class=BinarySensorDeviceClass.TAMPER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
name=f" {TAMPER}",
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
self._attr_is_on = self._location.is_cover_tampered()
|
||||
|
||||
|
||||
class TotalConnectAlarmPowerBinarySensor(TotalConnectAlarmBinarySensor):
|
||||
"""Represent an TotalConnect alarm power status."""
|
||||
|
||||
entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription(
|
||||
key=POWER,
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
name=f" {POWER}",
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
self._attr_is_on = not self._location.is_ac_loss()
|
||||
|
|
|
@ -148,12 +148,55 @@ PARTITIONS_UNKNOWN = {"PartitionInfo": PARTITION_INFO_UNKNOWN}
|
|||
|
||||
ZONE_NORMAL = {
|
||||
"ZoneID": "1",
|
||||
"ZoneDescription": "Normal",
|
||||
"ZoneStatus": ZoneStatus.NORMAL,
|
||||
"ZoneDescription": "Security",
|
||||
"ZoneStatus": ZoneStatus.FAULT,
|
||||
"ZoneTypeId": ZoneType.SECURITY,
|
||||
"PartitionId": "1",
|
||||
"CanBeBypassed": 1,
|
||||
}
|
||||
ZONE_2 = {
|
||||
"ZoneID": "2",
|
||||
"ZoneDescription": "Fire",
|
||||
"ZoneStatus": ZoneStatus.LOW_BATTERY,
|
||||
"ZoneTypeId": ZoneType.FIRE_SMOKE,
|
||||
"PartitionId": "1",
|
||||
"CanBeBypassed": 1,
|
||||
}
|
||||
ZONE_3 = {
|
||||
"ZoneID": "3",
|
||||
"ZoneDescription": "Gas",
|
||||
"ZoneStatus": ZoneStatus.TAMPER,
|
||||
"ZoneTypeId": ZoneType.CARBON_MONOXIDE,
|
||||
"PartitionId": "1",
|
||||
"CanBeBypassed": 1,
|
||||
}
|
||||
ZONE_4 = {
|
||||
"ZoneID": "4",
|
||||
"ZoneDescription": "Motion",
|
||||
"ZoneStatus": ZoneStatus.NORMAL,
|
||||
"ZoneTypeId": ZoneType.INTERIOR_FOLLOWER,
|
||||
"PartitionId": "1",
|
||||
"CanBeBypassed": 1,
|
||||
}
|
||||
ZONE_5 = {
|
||||
"ZoneID": "5",
|
||||
"ZoneDescription": "Medical",
|
||||
"ZoneStatus": ZoneStatus.NORMAL,
|
||||
"ZoneTypeId": ZoneType.PROA7_MEDICAL,
|
||||
"PartitionId": "1",
|
||||
"CanBeBypassed": 0,
|
||||
}
|
||||
# 99 is an unknown ZoneType
|
||||
ZONE_6 = {
|
||||
"ZoneID": "6",
|
||||
"ZoneDescription": "Medical",
|
||||
"ZoneStatus": ZoneStatus.NORMAL,
|
||||
"ZoneTypeId": 99,
|
||||
"PartitionId": "1",
|
||||
"CanBeBypassed": 0,
|
||||
}
|
||||
|
||||
ZONE_INFO = [ZONE_NORMAL]
|
||||
ZONE_INFO = [ZONE_NORMAL, ZONE_2, ZONE_3, ZONE_4, ZONE_5, ZONE_6]
|
||||
ZONES = {"ZoneInfo": ZONE_INFO}
|
||||
|
||||
METADATA_DISARMED = {
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
"""Tests for the TotalConnect binary sensor."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .common import LOCATION_ID, RESPONSE_DISARMED, ZONE_NORMAL, setup_platform
|
||||
|
||||
ZONE_ENTITY_ID = "binary_sensor.security"
|
||||
ZONE_LOW_BATTERY_ID = "binary_sensor.security_low_battery"
|
||||
ZONE_TAMPER_ID = "binary_sensor.security_tamper"
|
||||
PANEL_BATTERY_ID = "binary_sensor.test_low_battery"
|
||||
PANEL_TAMPER_ID = "binary_sensor.test_tamper"
|
||||
PANEL_POWER_ID = "binary_sensor.test_power"
|
||||
|
||||
|
||||
async def test_entity_registry(hass: HomeAssistant) -> None:
|
||||
"""Test the binary sensor is registered in entity registry."""
|
||||
await setup_platform(hass, BINARY_SENSOR)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# ensure zone 1 plus two diagnostic zones are created
|
||||
entry = entity_registry.async_get(ZONE_ENTITY_ID)
|
||||
entry_low_battery = entity_registry.async_get(ZONE_LOW_BATTERY_ID)
|
||||
entry_tamper = entity_registry.async_get(ZONE_TAMPER_ID)
|
||||
|
||||
assert entry.unique_id == f"{LOCATION_ID}_{ZONE_NORMAL['ZoneID']}_zone"
|
||||
assert (
|
||||
entry_low_battery.unique_id
|
||||
== f"{LOCATION_ID}_{ZONE_NORMAL['ZoneID']}_low_battery"
|
||||
)
|
||||
assert entry_tamper.unique_id == f"{LOCATION_ID}_{ZONE_NORMAL['ZoneID']}_tamper"
|
||||
|
||||
# ensure panel diagnostic zones are created
|
||||
panel_battery = entity_registry.async_get(PANEL_BATTERY_ID)
|
||||
panel_tamper = entity_registry.async_get(PANEL_TAMPER_ID)
|
||||
panel_power = entity_registry.async_get(PANEL_POWER_ID)
|
||||
|
||||
assert panel_battery.unique_id == f"{LOCATION_ID}_low_battery"
|
||||
assert panel_tamper.unique_id == f"{LOCATION_ID}_tamper"
|
||||
assert panel_power.unique_id == f"{LOCATION_ID}_power"
|
||||
|
||||
|
||||
async def test_state_and_attributes(hass: HomeAssistant) -> None:
|
||||
"""Test the binary sensor attributes are correct."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.totalconnect.TotalConnectClient.request",
|
||||
return_value=RESPONSE_DISARMED,
|
||||
):
|
||||
await setup_platform(hass, BINARY_SENSOR)
|
||||
|
||||
state = hass.states.get(ZONE_ENTITY_ID)
|
||||
assert state.state == STATE_ON
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME) == ZONE_NORMAL["ZoneDescription"]
|
||||
)
|
||||
assert state.attributes.get("device_class") == BinarySensorDeviceClass.DOOR
|
||||
|
||||
state = hass.states.get(f"{ZONE_ENTITY_ID}_low_battery")
|
||||
assert state.state == STATE_OFF
|
||||
state = hass.states.get(f"{ZONE_ENTITY_ID}_tamper")
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# Zone 2 is fire with low battery
|
||||
state = hass.states.get("binary_sensor.fire")
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("device_class") == BinarySensorDeviceClass.SMOKE
|
||||
state = hass.states.get("binary_sensor.fire_low_battery")
|
||||
assert state.state == STATE_ON
|
||||
state = hass.states.get("binary_sensor.fire_tamper")
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# Zone 3 is gas with tamper
|
||||
state = hass.states.get("binary_sensor.gas")
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("device_class") == BinarySensorDeviceClass.GAS
|
||||
state = hass.states.get("binary_sensor.gas_low_battery")
|
||||
assert state.state == STATE_OFF
|
||||
state = hass.states.get("binary_sensor.gas_tamper")
|
||||
assert state.state == STATE_ON
|
Loading…
Reference in New Issue