Skip to content

Commit

Permalink
Merge pull request #398 from ikalchev/v4.4.0
Browse files Browse the repository at this point in the history
V4.4.0
  • Loading branch information
ikalchev authored Jan 11, 2022
2 parents 341beee + 4c6fb25 commit 6c2b95c
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 35 additions & 10 deletions pyhap/characteristic.py
Original file line number Diff line number Diff line change
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
2 changes: 1 addition & 1 deletion pyhap/const.py
Original file line number Diff line number Diff line change
@@ -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}"
Expand Down
14 changes: 13 additions & 1 deletion tests/test_characteristic.py
Original file line number Diff line number Diff line change
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
6 changes: 3 additions & 3 deletions tests/test_hap_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down Expand Up @@ -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()


Expand All @@ -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()


Expand Down
6 changes: 3 additions & 3 deletions tests/test_hap_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 6c2b95c

Please sign in to comment.