Handle battery services that only report low battery in HomeKit Controller (#79072)

This commit is contained in:
J. Nick Koston 2022-09-25 12:08:28 -10:00 committed by GitHub
parent b70027aec1
commit 917cf674de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 485 additions and 5 deletions

View file

@ -10,9 +10,11 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import KNOWN_DEVICES
from .connection import HKDevice
from .entity import HomeKitEntity
@ -106,6 +108,29 @@ class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity):
return self.service.value(CharacteristicsTypes.LEAK_DETECTED) == 1
class HomeKitBatteryLowSensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit battery low sensor."""
_attr_device_class = BinarySensorDeviceClass.BATTERY
_attr_entity_category = EntityCategory.DIAGNOSTIC
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
return [CharacteristicsTypes.STATUS_LO_BATT]
@property
def name(self) -> str:
"""Return the name of the sensor."""
if name := self.accessory.name:
return f"{name} Low Battery"
return "Low Battery"
@property
def is_on(self) -> bool:
"""Return true if low battery is detected from the binary sensor."""
return self.service.value(CharacteristicsTypes.STATUS_LO_BATT) == 1
ENTITY_TYPES = {
ServicesTypes.MOTION_SENSOR: HomeKitMotionSensor,
ServicesTypes.CONTACT_SENSOR: HomeKitContactSensor,
@ -113,6 +138,17 @@ ENTITY_TYPES = {
ServicesTypes.CARBON_MONOXIDE_SENSOR: HomeKitCarbonMonoxideSensor,
ServicesTypes.OCCUPANCY_SENSOR: HomeKitOccupancySensor,
ServicesTypes.LEAK_SENSOR: HomeKitLeakSensor,
ServicesTypes.BATTERY_SERVICE: HomeKitBatteryLowSensor,
}
# Only create the entity if it has the required characteristic
REQUIRED_CHAR_BY_TYPE = {
ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.STATUS_LO_BATT,
}
# Reject the service as another platform can represent it better
# if it has a specific characteristic
REJECT_CHAR_BY_TYPE = {
ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.BATTERY_LEVEL,
}
@ -123,12 +159,20 @@ async def async_setup_entry(
) -> None:
"""Set up Homekit lighting."""
hkid = config_entry.data["AccessoryPairingID"]
conn = hass.data[KNOWN_DEVICES][hkid]
conn: HKDevice = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_service(service: Service) -> bool:
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
if (
required_char := REQUIRED_CHAR_BY_TYPE.get(service.type)
) and not service.has(required_char):
return False
if (reject_char := REJECT_CHAR_BY_TYPE.get(service.type)) and service.has(
reject_char
):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([entity_class(conn, info)], True)
return True

View file

@ -60,7 +60,7 @@ async def async_setup_entry(
) -> None:
"""Set up Homekit numbers."""
hkid = config_entry.data["AccessoryPairingID"]
conn = hass.data[KNOWN_DEVICES][hkid]
conn: HKDevice = hass.data[KNOWN_DEVICES][hkid]
@callback
def async_add_characteristic(char: Characteristic) -> bool:

View file

@ -410,6 +410,7 @@ class HomeKitBatterySensor(HomeKitSensor):
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
_attr_entity_category = EntityCategory.DIAGNOSTIC
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""
@ -517,6 +518,11 @@ ENTITY_TYPES = {
ServicesTypes.BATTERY_SERVICE: HomeKitBatterySensor,
}
# Only create the entity if it has the required characteristic
REQUIRED_CHAR_BY_TYPE = {
ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.BATTERY_LEVEL,
}
async def async_setup_entry(
hass: HomeAssistant,
@ -531,6 +537,10 @@ async def async_setup_entry(
def async_add_service(service: Service) -> bool:
if not (entity_class := ENTITY_TYPES.get(service.type)):
return False
if (
required_char := REQUIRED_CHAR_BY_TYPE.get(service.type)
) and not service.has(required_char):
return False
info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([entity_class(conn, info)], True)
return True

View file

@ -0,0 +1,362 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "000000A2-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "00000037-0000-1000-8000-0026BB765291",
"iid": 19,
"perms": ["pr"],
"format": "string",
"value": "2.2.0",
"description": "Version",
"maxLen": 64
},
{
"type": "000000A5-0000-1000-8000-0026BB765291",
"iid": 18,
"perms": ["pr"],
"format": "data",
"value": ""
}
]
},
{
"iid": 4,
"type": "EA22EA53-6227-55EA-AC24-73ACF3EEA0E8",
"characteristics": [
{
"type": "000000A5-0000-1000-8000-0026BB765291",
"iid": 340,
"perms": ["pr"],
"format": "data",
"value": ""
},
{
"type": "00F44C18-042E-5C4E-9A4C-561D44DCD804",
"iid": 339,
"perms": ["pr", "hd"],
"format": "string",
"value": "g8d8a6c",
"maxLen": 64
}
]
},
{
"iid": 7,
"type": "0000003E-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 2,
"perms": ["pr"],
"format": "string",
"value": "Smart CO Alarm",
"description": "Name",
"maxLen": 64
},
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 3,
"perms": ["pw"],
"format": "bool",
"description": "Identify"
},
{
"type": "00000053-0000-1000-8000-0026BB765291",
"iid": 8,
"perms": ["pr"],
"format": "string",
"value": "0",
"description": "Hardware Revision",
"maxLen": 64
},
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 4,
"perms": ["pr"],
"format": "string",
"value": "Netatmo",
"description": "Manufacturer",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 6,
"perms": ["pr"],
"format": "string",
"value": "1234",
"description": "Serial Number",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 5,
"perms": ["pr"],
"format": "string",
"value": "Smart CO Alarm",
"description": "Model",
"maxLen": 64
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 7,
"perms": ["pr"],
"format": "string",
"value": "1.0.3",
"description": "Firmware Revision",
"maxLen": 64
},
{
"type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B",
"iid": 47,
"perms": ["pr", "hd"],
"format": "string",
"value": "4.1;Sep 27 2021 12:54:49",
"maxLen": 64
},
{
"type": "00000220-0000-1000-8000-0026BB765291",
"iid": 325,
"perms": ["pr", "hd"],
"format": "data",
"value": "fa7beb3a4566c1fb"
}
]
},
{
"iid": 17,
"type": "00000055-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "0000004C-0000-1000-8000-0026BB765291",
"iid": 203,
"perms": [],
"format": "data",
"description": "Pair Setup"
},
{
"type": "0000004F-0000-1000-8000-0026BB765291",
"iid": 205,
"perms": [],
"format": "uint8",
"description": "Pairing Features"
},
{
"type": "0000004E-0000-1000-8000-0026BB765291",
"iid": 204,
"perms": [],
"format": "data",
"description": "Pair Verify"
},
{
"type": "00000050-0000-1000-8000-0026BB765291",
"iid": 206,
"perms": ["pr", "pw"],
"format": "data",
"value": null,
"description": "Pairing Pairings"
}
]
},
{
"iid": 22,
"type": "0000007F-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "00000075-0000-1000-8000-0026BB765291",
"iid": 230,
"perms": ["pr", "ev"],
"format": "bool",
"value": true,
"description": "Status Active"
},
{
"type": "00000077-0000-1000-8000-0026BB765291",
"iid": 231,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"description": "Status Fault",
"minValue": 0,
"maxValue": 1
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 42,
"perms": ["pr"],
"format": "string",
"value": "Carbon Monoxide Sensor",
"description": "Name",
"maxLen": 64
},
{
"type": "00000091-0000-1000-8000-0026BB765291",
"iid": 41,
"perms": ["pr", "ev"],
"format": "float",
"value": 0.0,
"description": "Carbon Monoxide Peak Level",
"minValue": 0.0,
"maxValue": 1000.0
},
{
"type": "00000019-4DDB-598F-A73F-006513F2DB6B",
"iid": 333,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"minValue": 0,
"maxValue": 3
},
{
"type": "00000090-0000-1000-8000-0026BB765291",
"iid": 40,
"perms": ["pr", "ev"],
"format": "float",
"value": 0.0,
"description": "Carbon Monoxide Level",
"minValue": 0.0,
"maxValue": 1000.0
},
{
"type": "0000000C-4DDB-598F-A73F-006513F2DB6B",
"iid": 326,
"perms": ["pr", "ev"],
"format": "bool",
"value": false
},
{
"type": "0000007A-0000-1000-8000-0026BB765291",
"iid": 45,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"description": "Status Tampered",
"minValue": 0,
"maxValue": 1
},
{
"type": "00000001-4DDB-598F-A73F-006513F2DB6B",
"iid": 327,
"perms": ["pr", "pw", "ev"],
"format": "bool",
"value": false
},
{
"type": "00000012-4DDB-598F-A73F-006513F2DB6B",
"iid": 328,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"minValue": 0,
"maxValue": 3
},
{
"type": "000000A5-0000-1000-8000-0026BB765291",
"iid": 43,
"perms": ["pr"],
"format": "data",
"value": ""
},
{
"type": "00000069-0000-1000-8000-0026BB765291",
"iid": 229,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"description": "Carbon Monoxide Detected",
"minValue": 0,
"maxValue": 1
}
]
},
{
"iid": 35,
"type": "00001801-0000-1000-8000-00805F9B34FB",
"characteristics": []
},
{
"iid": 36,
"type": "00000096-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 336,
"perms": ["pr"],
"format": "string",
"value": "Battery Service",
"description": "Name",
"maxLen": 64
},
{
"type": "00000079-0000-1000-8000-0026BB765291",
"iid": 337,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"description": "Status Low Battery",
"minValue": 0,
"maxValue": 1
},
{
"type": "00000002-4DDB-598F-A73F-006513F2DB6B",
"iid": 335,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"minValue": 0,
"maxValue": 4
}
]
},
{
"iid": 40,
"type": "00000004-4DDB-598F-A73F-006513F2DB6B",
"characteristics": [
{
"type": "00000007-4DDB-598F-A73F-006513F2DB6B",
"iid": 187,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 0,
"minValue": 0,
"maxValue": 14
},
{
"type": "000000A5-0000-1000-8000-0026BB765291",
"iid": 183,
"perms": ["pr"],
"format": "data",
"value": ""
},
{
"type": "00000009-4DDB-598F-A73F-006513F2DB6B",
"iid": 184,
"perms": ["pw", "hd"],
"format": "uint8",
"minValue": 0,
"maxValue": 1
},
{
"type": "00000006-4DDB-598F-A73F-006513F2DB6B",
"iid": 188,
"perms": ["pr"],
"format": "data",
"value": "0000000000000000"
},
{
"type": "0000000A-4DDB-598F-A73F-006513F2DB6B",
"iid": 324,
"perms": ["pr", "ev"],
"format": "uint32",
"value": 3
}
]
}
]
}
]

View file

@ -9,6 +9,7 @@ https://github.com/home-assistant/core/pull/39090
from homeassistant.components.sensor import SensorStateClass
from homeassistant.const import PERCENTAGE
from homeassistant.helpers.entity import EntityCategory
from ..common import (
HUB_TEST_ACCESSORY_ID,
@ -43,6 +44,7 @@ async def test_aqara_switch_setup(hass):
friendly_name="Programmable Switch Battery Sensor",
unique_id="homekit-111a1111a1a111-5",
capabilities={"state_class": SensorStateClass.MEASUREMENT},
entity_category=EntityCategory.DIAGNOSTIC,
unit_of_measurement=PERCENTAGE,
state="100",
),

View file

@ -2,6 +2,7 @@
from homeassistant.components.sensor import SensorStateClass
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
from homeassistant.helpers.entity import EntityCategory
from ..common import (
HUB_TEST_ACCESSORY_ID,
@ -46,6 +47,7 @@ async def test_arlo_baby_setup(hass):
entity_id="sensor.arlobabya0_battery",
unique_id="homekit-00A0000000000-700",
friendly_name="ArloBabyA0 Battery",
entity_category=EntityCategory.DIAGNOSTIC,
capabilities={"state_class": SensorStateClass.MEASUREMENT},
unit_of_measurement=PERCENTAGE,
state="82",

View file

@ -60,6 +60,7 @@ async def test_eve_degree_setup(hass):
entity_id="sensor.eve_degree_aa11_battery",
unique_id="homekit-AA00A0A00000-17",
friendly_name="Eve Degree AA11 Battery",
entity_category=EntityCategory.DIAGNOSTIC,
capabilities={"state_class": SensorStateClass.MEASUREMENT},
unit_of_measurement=PERCENTAGE,
state="65",

View file

@ -2,6 +2,7 @@
from homeassistant.components.sensor import SensorStateClass
from homeassistant.const import PERCENTAGE
from homeassistant.helpers.entity import EntityCategory
from ..common import (
HUB_TEST_ACCESSORY_ID,
@ -44,6 +45,7 @@ async def test_hue_bridge_setup(hass):
entity_id="sensor.hue_dimmer_switch_battery",
capabilities={"state_class": SensorStateClass.MEASUREMENT},
friendly_name="Hue dimmer switch battery",
entity_category=EntityCategory.DIAGNOSTIC,
unique_id="homekit-6623462389072572-644245094400",
unit_of_measurement=PERCENTAGE,
state="100",

View file

@ -0,0 +1,50 @@
"""
Regression tests for Netamo Smart CO Alarm.
https://github.com/home-assistant/core/issues/78903
"""
from homeassistant.helpers.entity import EntityCategory
from ..common import (
HUB_TEST_ACCESSORY_ID,
DeviceTestInfo,
EntityTestInfo,
assert_devices_and_entities_created,
setup_accessories_from_file,
setup_test_accessories,
)
async def test_netamo_smart_co_alarm_setup(hass):
"""Test that a Netamo Smart CO Alarm can be correctly setup in HA."""
accessories = await setup_accessories_from_file(hass, "netamo_smart_co_alarm.json")
await setup_test_accessories(hass, accessories)
await assert_devices_and_entities_created(
hass,
DeviceTestInfo(
unique_id=HUB_TEST_ACCESSORY_ID,
name="Smart CO Alarm",
model="Smart CO Alarm",
manufacturer="Netatmo",
sw_version="1.0.3",
hw_version="0",
serial_number="1234",
devices=[],
entities=[
EntityTestInfo(
entity_id="binary_sensor.smart_co_alarm_carbon_monoxide_sensor",
friendly_name="Smart CO Alarm Carbon Monoxide Sensor",
unique_id="homekit-1234-22",
state="off",
),
EntityTestInfo(
entity_id="binary_sensor.smart_co_alarm_low_battery",
friendly_name="Smart CO Alarm Low Battery",
entity_category=EntityCategory.DIAGNOSTIC,
unique_id="homekit-1234-36",
state="off",
),
],
),
)

View file

@ -3,6 +3,7 @@
from homeassistant.components.cover import CoverEntityFeature
from homeassistant.components.sensor import SensorStateClass
from homeassistant.const import PERCENTAGE
from homeassistant.helpers.entity import EntityCategory
from ..common import (
HUB_TEST_ACCESSORY_ID,
@ -53,6 +54,7 @@ async def test_ryse_smart_bridge_setup(hass):
EntityTestInfo(
entity_id="sensor.master_bath_south_ryse_shade_battery",
friendly_name="Master Bath South RYSE Shade Battery",
entity_category=EntityCategory.DIAGNOSTIC,
capabilities={"state_class": SensorStateClass.MEASUREMENT},
unique_id="homekit-00:00:00:00:00:00-2-64",
unit_of_measurement=PERCENTAGE,
@ -80,6 +82,7 @@ async def test_ryse_smart_bridge_setup(hass):
EntityTestInfo(
entity_id="sensor.ryse_smartshade_ryse_shade_battery",
friendly_name="RYSE SmartShade RYSE Shade Battery",
entity_category=EntityCategory.DIAGNOSTIC,
capabilities={"state_class": SensorStateClass.MEASUREMENT},
unique_id="homekit-00:00:00:00:00:00-3-64",
unit_of_measurement=PERCENTAGE,
@ -130,6 +133,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass):
EntityTestInfo(
entity_id="sensor.lr_left_ryse_shade_battery",
friendly_name="LR Left RYSE Shade Battery",
entity_category=EntityCategory.DIAGNOSTIC,
capabilities={"state_class": SensorStateClass.MEASUREMENT},
unique_id="homekit-00:00:00:00:00:00-2-64",
unit_of_measurement=PERCENTAGE,
@ -157,6 +161,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass):
EntityTestInfo(
entity_id="sensor.lr_right_ryse_shade_battery",
friendly_name="LR Right RYSE Shade Battery",
entity_category=EntityCategory.DIAGNOSTIC,
capabilities={"state_class": SensorStateClass.MEASUREMENT},
unique_id="homekit-00:00:00:00:00:00-3-64",
unit_of_measurement=PERCENTAGE,
@ -184,6 +189,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass):
EntityTestInfo(
entity_id="sensor.br_left_ryse_shade_battery",
friendly_name="BR Left RYSE Shade Battery",
entity_category=EntityCategory.DIAGNOSTIC,
capabilities={"state_class": SensorStateClass.MEASUREMENT},
unique_id="homekit-00:00:00:00:00:00-4-64",
unit_of_measurement=PERCENTAGE,
@ -210,6 +216,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass):
),
EntityTestInfo(
entity_id="sensor.rzss_ryse_shade_battery",
entity_category=EntityCategory.DIAGNOSTIC,
capabilities={"state_class": SensorStateClass.MEASUREMENT},
friendly_name="RZSS RYSE Shade Battery",
unique_id="homekit-00:00:00:00:00:00-5-64",

View file

@ -98,7 +98,7 @@ async def test_enumerate_remote(hass, utcnow):
"entity_id": "sensor.testdevice_battery",
"platform": "device",
"type": "battery_level",
"metadata": {"secondary": False},
"metadata": {"secondary": True},
},
{
"device_id": device.id,
@ -146,7 +146,7 @@ async def test_enumerate_button(hass, utcnow):
"entity_id": "sensor.testdevice_battery",
"platform": "device",
"type": "battery_level",
"metadata": {"secondary": False},
"metadata": {"secondary": True},
},
{
"device_id": device.id,
@ -193,7 +193,7 @@ async def test_enumerate_doorbell(hass, utcnow):
"entity_id": "sensor.testdevice_battery",
"platform": "device",
"type": "battery_level",
"metadata": {"secondary": False},
"metadata": {"secondary": True},
},
{
"device_id": device.id,