Add support for homekit valve accessories to homekit_controller (#32937)
This commit is contained in:
parent
0cb27ff236
commit
34e44e7f3a
5 changed files with 1351 additions and 17 deletions
|
@ -32,4 +32,5 @@ HOMEKIT_ACCESSORY_DISPATCH = {
|
|||
"air-quality": "air_quality",
|
||||
"occupancy": "binary_sensor",
|
||||
"television": "media_player",
|
||||
"valve": "switch",
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
"""Support for Homekit switches."""
|
||||
import logging
|
||||
|
||||
from aiohomekit.model.characteristics import CharacteristicsTypes
|
||||
from aiohomekit.model.characteristics import (
|
||||
CharacteristicsTypes,
|
||||
InUseValues,
|
||||
IsConfiguredValues,
|
||||
)
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.core import callback
|
||||
|
@ -12,21 +16,9 @@ OUTLET_IN_USE = "outlet_in_use"
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Homekit lock."""
|
||||
hkid = config_entry.data["AccessoryPairingID"]
|
||||
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||
|
||||
@callback
|
||||
def async_add_service(aid, service):
|
||||
if service["stype"] not in ("switch", "outlet"):
|
||||
return False
|
||||
info = {"aid": aid, "iid": service["iid"]}
|
||||
async_add_entities([HomeKitSwitch(conn, info)], True)
|
||||
return True
|
||||
|
||||
conn.add_listener(async_add_service)
|
||||
ATTR_IN_USE = "in_use"
|
||||
ATTR_IS_CONFIGURED = "is_configured"
|
||||
ATTR_REMAINING_DURATION = "remaining_duration"
|
||||
|
||||
|
||||
class HomeKitSwitch(HomeKitEntity, SwitchDevice):
|
||||
|
@ -55,3 +47,77 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice):
|
|||
outlet_in_use = self.service.value(CharacteristicsTypes.OUTLET_IN_USE)
|
||||
if outlet_in_use is not None:
|
||||
return {OUTLET_IN_USE: outlet_in_use}
|
||||
|
||||
|
||||
class HomeKitValve(HomeKitEntity, SwitchDevice):
|
||||
"""Represents a valve in an irrigation system."""
|
||||
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity cares about."""
|
||||
return [
|
||||
CharacteristicsTypes.ACTIVE,
|
||||
CharacteristicsTypes.IN_USE,
|
||||
CharacteristicsTypes.IS_CONFIGURED,
|
||||
CharacteristicsTypes.REMAINING_DURATION,
|
||||
]
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the specified valve on."""
|
||||
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True})
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the specified valve off."""
|
||||
await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
return "mdi:water"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self.service.value(CharacteristicsTypes.ACTIVE)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
attrs = {}
|
||||
|
||||
in_use = self.service.value(CharacteristicsTypes.IN_USE)
|
||||
if in_use is not None:
|
||||
attrs[ATTR_IN_USE] = in_use == InUseValues.IN_USE
|
||||
|
||||
is_configured = self.service.value(CharacteristicsTypes.IS_CONFIGURED)
|
||||
if is_configured is not None:
|
||||
attrs[ATTR_IS_CONFIGURED] = is_configured == IsConfiguredValues.CONFIGURED
|
||||
|
||||
remaining = self.service.value(CharacteristicsTypes.REMAINING_DURATION)
|
||||
if remaining is not None:
|
||||
attrs[ATTR_REMAINING_DURATION] = remaining
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
ENTITY_TYPES = {
|
||||
"switch": HomeKitSwitch,
|
||||
"outlet": HomeKitSwitch,
|
||||
"valve": HomeKitValve,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Homekit switches."""
|
||||
hkid = config_entry.data["AccessoryPairingID"]
|
||||
conn = hass.data[KNOWN_DEVICES][hkid]
|
||||
|
||||
@callback
|
||||
def async_add_service(aid, service):
|
||||
entity_class = ENTITY_TYPES.get(service["stype"])
|
||||
if not entity_class:
|
||||
return False
|
||||
info = {"aid": aid, "iid": service["iid"]}
|
||||
async_add_entities([entity_class(conn, info)], True)
|
||||
return True
|
||||
|
||||
conn.add_listener(async_add_service)
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
"""
|
||||
Make sure that existing RainMachine support isn't broken.
|
||||
|
||||
https://github.com/home-assistant/core/issues/31745
|
||||
"""
|
||||
|
||||
from tests.components.homekit_controller.common import (
|
||||
Helper,
|
||||
setup_accessories_from_file,
|
||||
setup_test_accessories,
|
||||
)
|
||||
|
||||
|
||||
async def test_rainmachine_pro_8_setup(hass):
|
||||
"""Test that a RainMachine can be correctly setup in HA."""
|
||||
accessories = await setup_accessories_from_file(hass, "rainmachine-pro-8.json")
|
||||
config_entry, pairing = await setup_test_accessories(hass, accessories)
|
||||
|
||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
# Assert that the entity is correctly added to the entity registry
|
||||
entry = entity_registry.async_get("switch.rainmachine_00ce4a")
|
||||
assert entry.unique_id == "homekit-00aa0000aa0a-512"
|
||||
|
||||
helper = Helper(
|
||||
hass, "switch.rainmachine_00ce4a", pairing, accessories[0], config_entry
|
||||
)
|
||||
state = await helper.poll_and_get_state()
|
||||
|
||||
# Assert that the friendly name is detected correctly
|
||||
assert state.attributes["friendly_name"] == "RainMachine-00ce4a"
|
||||
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
|
||||
device = device_registry.async_get(entry.device_id)
|
||||
assert device.manufacturer == "Green Electronics LLC"
|
||||
assert device.name == "RainMachine-00ce4a"
|
||||
assert device.model == "SPK5 Pro"
|
||||
assert device.sw_version == "1.0.4"
|
||||
assert device.via_device_id is None
|
||||
|
||||
# The device is made up of multiple valves - make sure we have enumerated them all
|
||||
entry = entity_registry.async_get("switch.rainmachine_00ce4a_2")
|
||||
assert entry.unique_id == "homekit-00aa0000aa0a-768"
|
||||
|
||||
entry = entity_registry.async_get("switch.rainmachine_00ce4a_3")
|
||||
assert entry.unique_id == "homekit-00aa0000aa0a-1024"
|
||||
|
||||
entry = entity_registry.async_get("switch.rainmachine_00ce4a_4")
|
||||
assert entry.unique_id == "homekit-00aa0000aa0a-1280"
|
||||
|
||||
entry = entity_registry.async_get("switch.rainmachine_00ce4a_5")
|
||||
assert entry.unique_id == "homekit-00aa0000aa0a-1536"
|
||||
|
||||
entry = entity_registry.async_get("switch.rainmachine_00ce4a_6")
|
||||
assert entry.unique_id == "homekit-00aa0000aa0a-1792"
|
||||
|
||||
entry = entity_registry.async_get("switch.rainmachine_00ce4a_7")
|
||||
assert entry.unique_id == "homekit-00aa0000aa0a-2048"
|
||||
|
||||
entry = entity_registry.async_get("switch.rainmachine_00ce4a_8")
|
||||
assert entry.unique_id == "homekit-00aa0000aa0a-2304"
|
||||
|
||||
entry = entity_registry.async_get("switch.rainmachine_00ce4a_9")
|
||||
assert entry is None
|
|
@ -1,6 +1,10 @@
|
|||
"""Basic checks for HomeKitSwitch."""
|
||||
|
||||
from aiohomekit.model.characteristics import CharacteristicsTypes
|
||||
from aiohomekit.model.characteristics import (
|
||||
CharacteristicsTypes,
|
||||
InUseValues,
|
||||
IsConfiguredValues,
|
||||
)
|
||||
from aiohomekit.model.services import ServicesTypes
|
||||
|
||||
from tests.components.homekit_controller.common import setup_test_component
|
||||
|
@ -17,6 +21,23 @@ def create_switch_service(accessory):
|
|||
outlet_in_use.value = False
|
||||
|
||||
|
||||
def create_valve_service(accessory):
|
||||
"""Define valve characteristics."""
|
||||
service = accessory.add_service(ServicesTypes.VALVE)
|
||||
|
||||
on_char = service.add_char(CharacteristicsTypes.ACTIVE)
|
||||
on_char.value = False
|
||||
|
||||
in_use = service.add_char(CharacteristicsTypes.IN_USE)
|
||||
in_use.value = InUseValues.IN_USE
|
||||
|
||||
configured = service.add_char(CharacteristicsTypes.IS_CONFIGURED)
|
||||
configured.value = IsConfiguredValues.CONFIGURED
|
||||
|
||||
remaining = service.add_char(CharacteristicsTypes.REMAINING_DURATION)
|
||||
remaining.value = 99
|
||||
|
||||
|
||||
async def test_switch_change_outlet_state(hass, utcnow):
|
||||
"""Test that we can turn a HomeKit outlet on and off again."""
|
||||
helper = await setup_test_component(hass, create_switch_service)
|
||||
|
@ -57,3 +78,47 @@ async def test_switch_read_outlet_state(hass, utcnow):
|
|||
switch_1 = await helper.poll_and_get_state()
|
||||
assert switch_1.state == "off"
|
||||
assert switch_1.attributes["outlet_in_use"] is True
|
||||
|
||||
|
||||
async def test_valve_change_active_state(hass, utcnow):
|
||||
"""Test that we can turn a valve on and off again."""
|
||||
helper = await setup_test_component(hass, create_valve_service)
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch", "turn_on", {"entity_id": "switch.testdevice"}, blocking=True
|
||||
)
|
||||
assert helper.characteristics[("valve", "active")].value == 1
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch", "turn_off", {"entity_id": "switch.testdevice"}, blocking=True
|
||||
)
|
||||
assert helper.characteristics[("valve", "active")].value == 0
|
||||
|
||||
|
||||
async def test_valve_read_state(hass, utcnow):
|
||||
"""Test that we can read the state of a valve accessory."""
|
||||
helper = await setup_test_component(hass, create_valve_service)
|
||||
|
||||
# Initial state is that the switch is off and the outlet isn't in use
|
||||
switch_1 = await helper.poll_and_get_state()
|
||||
assert switch_1.state == "off"
|
||||
assert switch_1.attributes["in_use"] is True
|
||||
assert switch_1.attributes["is_configured"] is True
|
||||
assert switch_1.attributes["remaining_duration"] == 99
|
||||
|
||||
# Simulate that someone switched on the device in the real world not via HA
|
||||
helper.characteristics[("valve", "active")].set_value(True)
|
||||
switch_1 = await helper.poll_and_get_state()
|
||||
assert switch_1.state == "on"
|
||||
|
||||
# Simulate that someone configured the device in the real world not via HA
|
||||
helper.characteristics[
|
||||
("valve", "is-configured")
|
||||
].value = IsConfiguredValues.NOT_CONFIGURED
|
||||
switch_1 = await helper.poll_and_get_state()
|
||||
assert switch_1.attributes["is_configured"] is False
|
||||
|
||||
# Simulate that someone using the device in the real world not via HA
|
||||
helper.characteristics[("valve", "in-use")].value = InUseValues.NOT_IN_USE
|
||||
switch_1 = await helper.poll_and_get_state()
|
||||
assert switch_1.attributes["in_use"] is False
|
||||
|
|
1137
tests/fixtures/homekit_controller/rainmachine-pro-8.json
vendored
Normal file
1137
tests/fixtures/homekit_controller/rainmachine-pro-8.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue