Skip to content

Commit

Permalink
Allow invalid client values when enabled
Browse files Browse the repository at this point in the history
As of iOS 15.1, Siri requests TargetHeatingCoolingState
as Auto reguardless if its a valid value or not.

Consumers of this api may wish to set allow_invalid_client_values
to True and handle converting the Auto state to Cool or Heat
depending on the device.

Reference: home-assistant/core#60203
  • Loading branch information
bdraco committed Nov 23, 2021
1 parent 7f14eb1 commit 626c033
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 11 deletions.
45 changes: 35 additions & 10 deletions pyhap/characteristic.py
Expand Up @@ -132,9 +132,12 @@ class Characteristic:
"service",
"_uuid_str",
"_loader_display_name",
"allow_invalid_client_values",
)

def __init__(self, display_name, type_id, properties):
def __init__(
self, display_name, type_id, properties, allow_invalid_client_values=False
):
"""Initialise with the given properties.
:param display_name: Name that will be displayed for this
Expand All @@ -150,6 +153,15 @@ def __init__(self, display_name, type_id, properties):
"""
_validate_properties(properties)
self.broker = None
#
# As of iOS 15.1, Siri requests TargetHeatingCoolingState
# as Auto reguardless if its a valid value or not.
#
# Consumers of this api may wish to set allow_invalid_client_values
# to True and handle converting the Auto state to Cool or Heat
# depending on the device.
#
self.allow_invalid_client_values = allow_invalid_client_values
self.display_name = display_name
self.properties = properties
self.type_id = type_id
Expand Down Expand Up @@ -185,14 +197,22 @@ def get_value(self):
self.value = self.to_valid_value(value=self.getter_callback())
return self.value

def valid_value_or_raise(self, value):
"""Raise ValueError if PROP_VALID_VALUES is set and the value is not present."""
if self.type_id in ALWAYS_NULL:
return
valid_values = self.properties.get(PROP_VALID_VALUES)
if not valid_values:
return
if value in valid_values.values():
return
error_msg = f"{self.display_name}: value={value} is an invalid value."
logger.error(error_msg)
raise ValueError(error_msg)

def to_valid_value(self, value):
"""Perform validation and conversion to valid value."""
if self.properties.get(PROP_VALID_VALUES):
if value not in self.properties[PROP_VALID_VALUES].values():
error_msg = f"{self.display_name}: value={value} is an invalid value."
logger.error(error_msg)
raise ValueError(error_msg)
elif self.properties[PROP_FORMAT] == HAP_FORMAT_STRING:
if self.properties[PROP_FORMAT] == HAP_FORMAT_STRING:
value = str(value)[
: self.properties.get(HAP_REPR_MAX_LEN, DEFAULT_MAX_LENGTH)
]
Expand Down Expand Up @@ -241,6 +261,7 @@ def override_properties(self, properties=None, valid_values=None):

try:
self.value = self.to_valid_value(self.value)
self.valid_value_or_raise(self.value)
except ValueError:
self.value = self._get_default_value()

Expand All @@ -265,6 +286,7 @@ def set_value(self, value, should_notify=True):
"""
logger.debug("set_value: %s to %s", self.display_name, value)
value = self.to_valid_value(value)
self.valid_value_or_raise(value)
changed = self.value != value
self.value = value
if changed and should_notify and self.broker:
Expand All @@ -280,20 +302,23 @@ def client_update_value(self, value, sender_client_addr=None):
original_value = value
if self.type_id not in ALWAYS_NULL or original_value is not None:
value = self.to_valid_value(value)
if not self.allow_invalid_client_values:
self.valid_value_or_raise(value)
logger.debug(
"client_update_value: %s to %s (original: %s) from client: %s",
self.display_name,
value,
original_value,
sender_client_addr,
)
changed = self.value != value
previous_value = self.value
self.value = value
if changed:
self.notify(sender_client_addr)
if self.setter_callback:
# pylint: disable=not-callable
self.setter_callback(value)
changed = self.value != previous_value
if changed:
self.notify(sender_client_addr)
if self.type_id in ALWAYS_NULL:
self.value = None

Expand Down
14 changes: 13 additions & 1 deletion tests/test_characteristic.py
Expand Up @@ -72,7 +72,7 @@ def test_to_valid_value():
PROPERTIES.copy(), valid={"foo": 2, "bar": 3}, min_value=2, max_value=7
)
with pytest.raises(ValueError):
char.to_valid_value(1)
char.valid_value_or_raise(1)
assert char.to_valid_value(2) == 2

del char.properties["ValidValues"]
Expand Down Expand Up @@ -353,6 +353,18 @@ def test_client_update_value():
assert len(mock_notify.mock_calls) == 3


def test_client_update_value_with_invalid_value():
"""Test updating the characteristic value with call from the driver with invalid values."""
char = get_char(PROPERTIES.copy(), valid={"foo": 0, "bar": 2, "baz": 1})

with patch.object(char, "broker"):
with pytest.raises(ValueError):
char.client_update_value(4)

char.allow_invalid_client_values = True
char.client_update_value(4)


def test_notify():
"""Test if driver is notified correctly about a changed characteristic."""
char = get_char(PROPERTIES.copy())
Expand Down

0 comments on commit 626c033

Please sign in to comment.