Skip to content

Commit 6c2b95c

Browse files
authored
Merge pull request #398 from ikalchev/v4.4.0
V4.4.0
2 parents 341beee + 4c6fb25 commit 6c2b95c

6 files changed

Lines changed: 59 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Sections
1515
### Breaking Changes
1616
### Developers
1717
-->
18+
## [4.4.0] - 2022-11-01
19+
20+
### Added
21+
- Allow invalid client values when enabled. [#392](https://github.com/ikalchev/HAP- python/pull/392)
1822

1923
## [4.3.0] - 2021-10-07
2024

pyhap/characteristic.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,12 @@ class Characteristic:
132132
"service",
133133
"_uuid_str",
134134
"_loader_display_name",
135+
"allow_invalid_client_values",
135136
)
136137

137-
def __init__(self, display_name, type_id, properties):
138+
def __init__(
139+
self, display_name, type_id, properties, allow_invalid_client_values=False
140+
):
138141
"""Initialise with the given properties.
139142
140143
:param display_name: Name that will be displayed for this
@@ -150,6 +153,15 @@ def __init__(self, display_name, type_id, properties):
150153
"""
151154
_validate_properties(properties)
152155
self.broker = None
156+
#
157+
# As of iOS 15.1, Siri requests TargetHeatingCoolingState
158+
# as Auto reguardless if its a valid value or not.
159+
#
160+
# Consumers of this api may wish to set allow_invalid_client_values
161+
# to True and handle converting the Auto state to Cool or Heat
162+
# depending on the device.
163+
#
164+
self.allow_invalid_client_values = allow_invalid_client_values
153165
self.display_name = display_name
154166
self.properties = properties
155167
self.type_id = type_id
@@ -185,14 +197,22 @@ def get_value(self):
185197
self.value = self.to_valid_value(value=self.getter_callback())
186198
return self.value
187199

200+
def valid_value_or_raise(self, value):
201+
"""Raise ValueError if PROP_VALID_VALUES is set and the value is not present."""
202+
if self.type_id in ALWAYS_NULL:
203+
return
204+
valid_values = self.properties.get(PROP_VALID_VALUES)
205+
if not valid_values:
206+
return
207+
if value in valid_values.values():
208+
return
209+
error_msg = f"{self.display_name}: value={value} is an invalid value."
210+
logger.error(error_msg)
211+
raise ValueError(error_msg)
212+
188213
def to_valid_value(self, value):
189214
"""Perform validation and conversion to valid value."""
190-
if self.properties.get(PROP_VALID_VALUES):
191-
if value not in self.properties[PROP_VALID_VALUES].values():
192-
error_msg = f"{self.display_name}: value={value} is an invalid value."
193-
logger.error(error_msg)
194-
raise ValueError(error_msg)
195-
elif self.properties[PROP_FORMAT] == HAP_FORMAT_STRING:
215+
if self.properties[PROP_FORMAT] == HAP_FORMAT_STRING:
196216
value = str(value)[
197217
: self.properties.get(HAP_REPR_MAX_LEN, DEFAULT_MAX_LENGTH)
198218
]
@@ -241,6 +261,7 @@ def override_properties(self, properties=None, valid_values=None):
241261

242262
try:
243263
self.value = self.to_valid_value(self.value)
264+
self.valid_value_or_raise(self.value)
244265
except ValueError:
245266
self.value = self._get_default_value()
246267

@@ -265,6 +286,7 @@ def set_value(self, value, should_notify=True):
265286
"""
266287
logger.debug("set_value: %s to %s", self.display_name, value)
267288
value = self.to_valid_value(value)
289+
self.valid_value_or_raise(value)
268290
changed = self.value != value
269291
self.value = value
270292
if changed and should_notify and self.broker:
@@ -280,20 +302,23 @@ def client_update_value(self, value, sender_client_addr=None):
280302
original_value = value
281303
if self.type_id not in ALWAYS_NULL or original_value is not None:
282304
value = self.to_valid_value(value)
305+
if not self.allow_invalid_client_values:
306+
self.valid_value_or_raise(value)
283307
logger.debug(
284308
"client_update_value: %s to %s (original: %s) from client: %s",
285309
self.display_name,
286310
value,
287311
original_value,
288312
sender_client_addr,
289313
)
290-
changed = self.value != value
314+
previous_value = self.value
291315
self.value = value
292-
if changed:
293-
self.notify(sender_client_addr)
294316
if self.setter_callback:
295317
# pylint: disable=not-callable
296318
self.setter_callback(value)
319+
changed = self.value != previous_value
320+
if changed:
321+
self.notify(sender_client_addr)
297322
if self.type_id in ALWAYS_NULL:
298323
self.value = None
299324

pyhap/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""This module contains constants used by other modules."""
22
MAJOR_VERSION = 4
3-
MINOR_VERSION = 3
3+
MINOR_VERSION = 4
44
PATCH_VERSION = 0
55
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
66
__version__ = f"{__short_version__}.{PATCH_VERSION}"

tests/test_characteristic.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def test_to_valid_value():
7272
PROPERTIES.copy(), valid={"foo": 2, "bar": 3}, min_value=2, max_value=7
7373
)
7474
with pytest.raises(ValueError):
75-
char.to_valid_value(1)
75+
char.valid_value_or_raise(1)
7676
assert char.to_valid_value(2) == 2
7777

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

355355

356+
def test_client_update_value_with_invalid_value():
357+
"""Test updating the characteristic value with call from the driver with invalid values."""
358+
char = get_char(PROPERTIES.copy(), valid={"foo": 0, "bar": 2, "baz": 1})
359+
360+
with patch.object(char, "broker"):
361+
with pytest.raises(ValueError):
362+
char.client_update_value(4)
363+
364+
char.allow_invalid_client_values = True
365+
char.client_update_value(4)
366+
367+
356368
def test_notify():
357369
"""Test if driver is notified correctly about a changed characteristic."""
358370
char = get_char(PROPERTIES.copy())

tests/test_hap_protocol.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def test_http10_close(driver):
114114

115115
assert writer.call_args_list[0][0][0].startswith(b"HTTP/1.1 200 OK\r\n") is True
116116
assert len(writer.call_args_list) == 1
117-
assert connections == {}
117+
assert not connections
118118
hap_proto.close()
119119

120120

@@ -143,7 +143,7 @@ def test_invalid_content_length(driver):
143143
is True
144144
)
145145
assert len(writer.call_args_list) == 1
146-
assert connections == {}
146+
assert not connections
147147
hap_proto.close()
148148

149149

@@ -165,7 +165,7 @@ def test_invalid_client_closes_connection(driver):
165165

166166
assert writer.call_args_list[0][0][0].startswith(b"HTTP/1.1 200 OK\r\n") is True
167167
assert len(writer.call_args_list) == 1
168-
assert connections == {}
168+
assert not connections
169169
hap_proto.close()
170170

171171

tests/test_hap_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ async def test_we_can_connect():
4141
server = hap_server.HAPServer(addr_info, driver)
4242
await server.async_start(loop)
4343
sock = server.server.sockets[0]
44-
assert server.connections == {}
44+
assert not server.connections
4545
_, port = sock.getsockname()
4646
_, writer = await asyncio.open_connection("127.0.0.1", port)
4747
# flush out any call_soon
4848
for _ in range(3):
4949
await asyncio.sleep(0)
50-
assert server.connections != {}
50+
assert server.connections
5151
server.async_stop()
5252
writer.close()
5353

@@ -138,7 +138,7 @@ def _save_event(hap_event):
138138
)
139139

140140
await asyncio.sleep(0)
141-
assert hap_events == []
141+
assert not hap_events
142142

143143
# Ensure that a the event is not sent if its unsubscribed during
144144
# the coalesce delay

0 commit comments

Comments
 (0)