diff --git a/pyhap/characteristic.py b/pyhap/characteristic.py index eea50fbb..02f85675 100644 --- a/pyhap/characteristic.py +++ b/pyhap/characteristic.py @@ -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 @@ -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 @@ -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) ] @@ -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() @@ -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: @@ -280,6 +302,8 @@ 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, @@ -287,13 +311,14 @@ def client_update_value(self, value, sender_client_addr=None): 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 diff --git a/tests/test_characteristic.py b/tests/test_characteristic.py index 25a8b5ba..828e73f5 100644 --- a/tests/test_characteristic.py +++ b/tests/test_characteristic.py @@ -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"] @@ -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())