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",
|
"air-quality": "air_quality",
|
||||||
"occupancy": "binary_sensor",
|
"occupancy": "binary_sensor",
|
||||||
"television": "media_player",
|
"television": "media_player",
|
||||||
|
"valve": "switch",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
"""Support for Homekit switches."""
|
"""Support for Homekit switches."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohomekit.model.characteristics import CharacteristicsTypes
|
from aiohomekit.model.characteristics import (
|
||||||
|
CharacteristicsTypes,
|
||||||
|
InUseValues,
|
||||||
|
IsConfiguredValues,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
@ -12,21 +16,9 @@ OUTLET_IN_USE = "outlet_in_use"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ATTR_IN_USE = "in_use"
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
ATTR_IS_CONFIGURED = "is_configured"
|
||||||
"""Set up Homekit lock."""
|
ATTR_REMAINING_DURATION = "remaining_duration"
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class HomeKitSwitch(HomeKitEntity, SwitchDevice):
|
class HomeKitSwitch(HomeKitEntity, SwitchDevice):
|
||||||
|
@ -55,3 +47,77 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice):
|
||||||
outlet_in_use = self.service.value(CharacteristicsTypes.OUTLET_IN_USE)
|
outlet_in_use = self.service.value(CharacteristicsTypes.OUTLET_IN_USE)
|
||||||
if outlet_in_use is not None:
|
if outlet_in_use is not None:
|
||||||
return {OUTLET_IN_USE: outlet_in_use}
|
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."""
|
"""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 aiohomekit.model.services import ServicesTypes
|
||||||
|
|
||||||
from tests.components.homekit_controller.common import setup_test_component
|
from tests.components.homekit_controller.common import setup_test_component
|
||||||
|
@ -17,6 +21,23 @@ def create_switch_service(accessory):
|
||||||
outlet_in_use.value = False
|
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):
|
async def test_switch_change_outlet_state(hass, utcnow):
|
||||||
"""Test that we can turn a HomeKit outlet on and off again."""
|
"""Test that we can turn a HomeKit outlet on and off again."""
|
||||||
helper = await setup_test_component(hass, create_switch_service)
|
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()
|
switch_1 = await helper.poll_and_get_state()
|
||||||
assert switch_1.state == "off"
|
assert switch_1.state == "off"
|
||||||
assert switch_1.attributes["outlet_in_use"] is True
|
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