diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a2322b..fcc79a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ Sections ### Breaking Changes ### Developers --> +## [4.4.0] - 2022-11-01 + +### Added +- Allow invalid client values when enabled. [#392](https://github.com/ikalchev/HAP- python/pull/392) ## [4.3.0] - 2021-10-07 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/pyhap/const.py b/pyhap/const.py index 25834be8..b3509a06 100644 --- a/pyhap/const.py +++ b/pyhap/const.py @@ -1,6 +1,6 @@ """This module contains constants used by other modules.""" MAJOR_VERSION = 4 -MINOR_VERSION = 3 +MINOR_VERSION = 4 PATCH_VERSION = 0 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" 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()) diff --git a/tests/test_hap_protocol.py b/tests/test_hap_protocol.py index 69e8a4b4..80f875e9 100644 --- a/tests/test_hap_protocol.py +++ b/tests/test_hap_protocol.py @@ -114,7 +114,7 @@ def test_http10_close(driver): assert writer.call_args_list[0][0][0].startswith(b"HTTP/1.1 200 OK\r\n") is True assert len(writer.call_args_list) == 1 - assert connections == {} + assert not connections hap_proto.close() @@ -143,7 +143,7 @@ def test_invalid_content_length(driver): is True ) assert len(writer.call_args_list) == 1 - assert connections == {} + assert not connections hap_proto.close() @@ -165,7 +165,7 @@ def test_invalid_client_closes_connection(driver): assert writer.call_args_list[0][0][0].startswith(b"HTTP/1.1 200 OK\r\n") is True assert len(writer.call_args_list) == 1 - assert connections == {} + assert not connections hap_proto.close() diff --git a/tests/test_hap_server.py b/tests/test_hap_server.py index ad929f53..46c8884d 100644 --- a/tests/test_hap_server.py +++ b/tests/test_hap_server.py @@ -41,13 +41,13 @@ async def test_we_can_connect(): server = hap_server.HAPServer(addr_info, driver) await server.async_start(loop) sock = server.server.sockets[0] - assert server.connections == {} + assert not server.connections _, port = sock.getsockname() _, writer = await asyncio.open_connection("127.0.0.1", port) # flush out any call_soon for _ in range(3): await asyncio.sleep(0) - assert server.connections != {} + assert server.connections server.async_stop() writer.close() @@ -138,7 +138,7 @@ def _save_event(hap_event): ) await asyncio.sleep(0) - assert hap_events == [] + assert not hap_events # Ensure that a the event is not sent if its unsubscribed during # the coalesce delay