Skip to content

Commit e077ce5

Browse files
authored
Fix hashing of accessories to not include the value (#464)
1 parent 07df76b commit e077ce5

File tree

6 files changed

+287
-79
lines changed

6 files changed

+287
-79
lines changed

pyhap/accessory.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def get_characteristic(self, aid: int, iid: int) -> Optional["Characteristic"]:
222222

223223
return self.iid_manager.get_obj(iid)
224224

225-
def to_HAP(self) -> Dict[str, Any]:
225+
def to_HAP(self, include_value: bool = True) -> Dict[str, Any]:
226226
"""A HAP representation of this Accessory.
227227
228228
:return: A HAP representation of this accessory. For example:
@@ -241,7 +241,7 @@ def to_HAP(self) -> Dict[str, Any]:
241241
"""
242242
return {
243243
HAP_REPR_AID: self.aid,
244-
HAP_REPR_SERVICES: [s.to_HAP() for s in self.services],
244+
HAP_REPR_SERVICES: [s.to_HAP(include_value=include_value) for s in self.services],
245245
}
246246

247247
def setup_message(self):
@@ -386,12 +386,12 @@ def add_accessory(self, acc: "Accessory") -> None:
386386

387387
self.accessories[acc.aid] = acc
388388

389-
def to_HAP(self) -> List[Dict[str, Any]]:
389+
def to_HAP(self, include_value: bool = True) -> List[Dict[str, Any]]:
390390
"""Returns a HAP representation of itself and all contained accessories.
391391
392392
.. seealso:: Accessory.to_HAP
393393
"""
394-
return [acc.to_HAP() for acc in (super(), *self.accessories.values())]
394+
return [acc.to_HAP(include_value=include_value) for acc in (super(), *self.accessories.values())]
395395

396396
def get_characteristic(self, aid: int, iid: int) -> Optional["Characteristic"]:
397397
""".. seealso:: Accessory.to_HAP"""

pyhap/accessory_driver.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -745,11 +745,18 @@ def setup_srp_verifier(self):
745745
@property
746746
def accessories_hash(self):
747747
"""Hash the get_accessories response to track configuration changes."""
748+
# We pass include_value=False to avoid including the value
749+
# of the characteristics in the hash. This is because the
750+
# value of the characteristics is not used by iOS to determine
751+
# if the accessory configuration has changed. It only uses the
752+
# characteristics metadata. If we included the value in the hash
753+
# then iOS would think the accessory configuration has changed
754+
# every time a characteristic value changed.
748755
return hashlib.sha512(
749-
util.to_sorted_hap_json(self.get_accessories())
756+
util.to_sorted_hap_json(self.get_accessories(include_value=False))
750757
).hexdigest()
751758

752-
def get_accessories(self):
759+
def get_accessories(self, include_value: bool = True):
753760
"""Returns the accessory in HAP format.
754761
755762
:return: An example HAP representation is:
@@ -774,7 +781,7 @@ def get_accessories(self):
774781
775782
:rtype: dict
776783
"""
777-
hap_rep = self.accessory.to_HAP()
784+
hap_rep = self.accessory.to_HAP(include_value=include_value)
778785
if not isinstance(hap_rep, list):
779786
hap_rep = [
780787
hap_rep,

pyhap/characteristic.py

Lines changed: 121 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,20 @@ class Characteristic:
125125

126126
__slots__ = (
127127
"broker",
128-
"display_name",
129-
"properties",
128+
"_display_name",
129+
"_properties",
130130
"type_id",
131-
"value",
131+
"_value",
132132
"getter_callback",
133133
"setter_callback",
134134
"service",
135135
"_uuid_str",
136136
"_loader_display_name",
137137
"allow_invalid_client_values",
138138
"unique_id",
139+
"_to_hap_cache_with_value",
140+
"_to_hap_cache",
141+
"_always_null",
139142
)
140143

141144
def __init__(
@@ -169,34 +172,68 @@ def __init__(
169172
# to True and handle converting the Auto state to Cool or Heat
170173
# depending on the device.
171174
#
175+
self._always_null = type_id in ALWAYS_NULL
172176
self.allow_invalid_client_values = allow_invalid_client_values
173-
self.display_name = display_name
174-
self.properties: Dict[str, Any] = properties
177+
self._display_name = display_name
178+
self._properties: Dict[str, Any] = properties
175179
self.type_id = type_id
176-
self.value = self._get_default_value()
180+
self._value = self._get_default_value()
177181
self.getter_callback: Optional[Callable[[], Any]] = None
178182
self.setter_callback: Optional[Callable[[Any], None]] = None
179183
self.service: Optional["Service"] = None
180184
self.unique_id = unique_id
181185
self._uuid_str = uuid_to_hap_type(type_id)
182186
self._loader_display_name: Optional[str] = None
187+
self._to_hap_cache_with_value: Optional[Dict[str, Any]] = None
188+
self._to_hap_cache: Optional[Dict[str, Any]] = None
189+
190+
@property
191+
def display_name(self) -> Optional[str]:
192+
"""Return the display name of the characteristic."""
193+
return self._display_name
194+
195+
@display_name.setter
196+
def display_name(self, value: str) -> None:
197+
"""Set the display name of the characteristic."""
198+
self._display_name = value
199+
self._clear_cache()
200+
201+
@property
202+
def value(self) -> Any:
203+
"""Return the value of the characteristic."""
204+
return self._value
205+
206+
@value.setter
207+
def value(self, value: Any) -> None:
208+
"""Set the value of the characteristic."""
209+
self._value = value
210+
self._clear_cache()
211+
212+
@property
213+
def properties(self) -> Dict[str, Any]:
214+
"""Return the properties of the characteristic.
215+
216+
Properties should not be modified directly. Use override_properties instead.
217+
"""
218+
return self._properties
183219

184220
def __repr__(self) -> str:
185221
"""Return the representation of the characteristic."""
186222
return (
187-
f"<characteristic display_name={self.display_name} unique_id={self.unique_id} "
188-
f"value={self.value} properties={self.properties}>"
223+
f"<characteristic display_name={self._display_name} unique_id={self.unique_id} "
224+
f"value={self._value} properties={self._properties}>"
189225
)
190226

191227
def _get_default_value(self) -> Any:
192228
"""Return default value for format."""
193-
if self.type_id in ALWAYS_NULL:
229+
if self._always_null:
194230
return None
195231

196-
if self.properties.get(PROP_VALID_VALUES):
197-
return min(self.properties[PROP_VALID_VALUES].values())
232+
valid_values = self._properties.get(PROP_VALID_VALUES)
233+
if valid_values:
234+
return min(valid_values.values())
198235

199-
value = HAP_FORMAT_DEFAULTS[self.properties[PROP_FORMAT]]
236+
value = HAP_FORMAT_DEFAULTS[self._properties[PROP_FORMAT]]
200237
return self.to_valid_value(value)
201238

202239
def get_value(self) -> Any:
@@ -207,43 +244,47 @@ def get_value(self) -> Any:
207244
if self.getter_callback:
208245
# pylint: disable=not-callable
209246
self.value = self.to_valid_value(value=self.getter_callback())
210-
return self.value
247+
return self._value
211248

212249
def valid_value_or_raise(self, value: Any) -> None:
213250
"""Raise ValueError if PROP_VALID_VALUES is set and the value is not present."""
214-
if self.type_id in ALWAYS_NULL:
251+
if self._always_null:
215252
return
216-
valid_values = self.properties.get(PROP_VALID_VALUES)
253+
valid_values = self._properties.get(PROP_VALID_VALUES)
217254
if not valid_values:
218255
return
219256
if value in valid_values.values():
220257
return
221-
error_msg = f"{self.display_name}: value={value} is an invalid value."
258+
error_msg = f"{self._display_name}: value={value} is an invalid value."
222259
logger.error(error_msg)
223260
raise ValueError(error_msg)
224261

225262
def to_valid_value(self, value: Any) -> Any:
226263
"""Perform validation and conversion to valid value."""
227-
if self.properties[PROP_FORMAT] == HAP_FORMAT_STRING:
228-
value = str(value)[
229-
: self.properties.get(HAP_REPR_MAX_LEN, DEFAULT_MAX_LENGTH)
230-
]
231-
elif self.properties[PROP_FORMAT] == HAP_FORMAT_BOOL:
232-
value = bool(value)
233-
elif self.properties[PROP_FORMAT] in HAP_FORMAT_NUMERICS:
264+
properties = self._properties
265+
prop_format = properties[PROP_FORMAT]
266+
267+
if prop_format == HAP_FORMAT_STRING:
268+
return str(value)[: properties.get(HAP_REPR_MAX_LEN, DEFAULT_MAX_LENGTH)]
269+
270+
if prop_format == HAP_FORMAT_BOOL:
271+
return bool(value)
272+
273+
if prop_format in HAP_FORMAT_NUMERICS:
234274
if not isinstance(value, (int, float)):
235275
error_msg = (
236-
f"{self.display_name}: value={value} is not a numeric value."
276+
f"{self._display_name}: value={value} is not a numeric value."
237277
)
238278
logger.error(error_msg)
239279
raise ValueError(error_msg)
240-
min_step = self.properties.get(PROP_MIN_STEP)
280+
min_step = properties.get(PROP_MIN_STEP)
241281
if value and min_step:
242282
value = round(min_step * round(value / min_step), 14)
243-
value = min(self.properties.get(PROP_MAX_VALUE, value), value)
244-
value = max(self.properties.get(PROP_MIN_VALUE, value), value)
245-
if self.properties[PROP_FORMAT] != HAP_FORMAT_FLOAT:
246-
value = int(value)
283+
value = min(properties.get(PROP_MAX_VALUE, value), value)
284+
value = max(properties.get(PROP_MIN_VALUE, value), value)
285+
if prop_format != HAP_FORMAT_FLOAT:
286+
return int(value)
287+
247288
return value
248289

249290
def override_properties(
@@ -264,23 +305,30 @@ def override_properties(
264305
if not properties and not valid_values:
265306
raise ValueError("No properties or valid_values specified to override.")
266307

308+
self._clear_cache()
309+
267310
if properties:
268311
_validate_properties(properties)
269-
self.properties.update(properties)
312+
self._properties.update(properties)
270313

271314
if valid_values:
272-
self.properties[PROP_VALID_VALUES] = valid_values
315+
self._properties[PROP_VALID_VALUES] = valid_values
273316

274-
if self.type_id in ALWAYS_NULL:
317+
if self._always_null:
275318
self.value = None
276319
return
277320

278321
try:
279-
self.value = self.to_valid_value(self.value)
280-
self.valid_value_or_raise(self.value)
322+
self.value = self.to_valid_value(self._value)
323+
self.valid_value_or_raise(self._value)
281324
except ValueError:
282325
self.value = self._get_default_value()
283326

327+
def _clear_cache(self) -> None:
328+
"""Clear the cached HAP representation."""
329+
self._to_hap_cache = None
330+
self._to_hap_cache_with_value = None
331+
284332
def set_value(self, value: Any, should_notify: bool = True) -> None:
285333
"""Set the given raw value. It is checked if it is a valid value.
286334
@@ -300,14 +348,14 @@ def set_value(self, value: Any, should_notify: bool = True) -> None:
300348
subscribed clients. Notify will be performed if the broker is set.
301349
:type should_notify: bool
302350
"""
303-
logger.debug("set_value: %s to %s", self.display_name, value)
351+
logger.debug("set_value: %s to %s", self._display_name, value)
304352
value = self.to_valid_value(value)
305353
self.valid_value_or_raise(value)
306-
changed = self.value != value
354+
changed = self._value != value
307355
self.value = value
308356
if changed and should_notify and self.broker:
309357
self.notify()
310-
if self.type_id in ALWAYS_NULL:
358+
if self._always_null:
311359
self.value = None
312360

313361
def client_update_value(
@@ -318,27 +366,27 @@ def client_update_value(
318366
Change self.value to value and call callback.
319367
"""
320368
original_value = value
321-
if self.type_id not in ALWAYS_NULL or original_value is not None:
369+
if not self._always_null or original_value is not None:
322370
value = self.to_valid_value(value)
323371
if not self.allow_invalid_client_values:
324372
self.valid_value_or_raise(value)
325373
logger.debug(
326374
"client_update_value: %s to %s (original: %s) from client: %s",
327-
self.display_name,
375+
self._display_name,
328376
value,
329377
original_value,
330378
sender_client_addr,
331379
)
332-
previous_value = self.value
380+
previous_value = self._value
333381
self.value = value
334382
response = None
335383
if self.setter_callback:
336384
# pylint: disable=not-callable
337385
response = self.setter_callback(value)
338-
changed = self.value != previous_value
386+
changed = self._value != previous_value
339387
if changed:
340388
self.notify(sender_client_addr)
341-
if self.type_id in ALWAYS_NULL:
389+
if self._always_null:
342390
self.value = None
343391
return response
344392

@@ -352,49 +400,59 @@ def notify(self, sender_client_addr: Optional[Tuple[str, int]] = None) -> None:
352400
self.broker.publish(self.value, self, sender_client_addr, immediate)
353401

354402
# pylint: disable=invalid-name
355-
def to_HAP(self) -> Dict[str, Any]:
403+
def to_HAP(self, include_value: bool = True) -> Dict[str, Any]:
356404
"""Create a HAP representation of this Characteristic.
357405
358406
Used for json serialization.
359407
360408
:return: A HAP representation.
361409
:rtype: dict
362410
"""
411+
if include_value:
412+
if self._to_hap_cache_with_value is not None and not self.getter_callback:
413+
return self._to_hap_cache_with_value
414+
elif self._to_hap_cache is not None:
415+
return self._to_hap_cache
416+
417+
properties = self._properties
418+
permissions = properties[PROP_PERMISSIONS]
419+
prop_format = properties[PROP_FORMAT]
363420
hap_rep = {
364421
HAP_REPR_IID: self.broker.iid_manager.get_iid(self),
365422
HAP_REPR_TYPE: self._uuid_str,
366-
HAP_REPR_PERM: self.properties[PROP_PERMISSIONS],
367-
HAP_REPR_FORMAT: self.properties[PROP_FORMAT],
423+
HAP_REPR_PERM: permissions,
424+
HAP_REPR_FORMAT: prop_format,
368425
}
369426
# HAP_REPR_DESC (description) is optional and takes up
370427
# quite a bit of space in the payload. Only include it
371428
# if it has been changed from the default loader version
372-
if (
373-
not self._loader_display_name
374-
or self._loader_display_name != self.display_name
375-
):
376-
hap_rep[HAP_REPR_DESC] = self.display_name
377-
378-
value = self.get_value()
379-
if self.properties[PROP_FORMAT] in HAP_FORMAT_NUMERICS:
429+
loader_display_name = self._loader_display_name
430+
display_name = self._display_name
431+
if not loader_display_name or loader_display_name != display_name:
432+
hap_rep[HAP_REPR_DESC] = display_name
433+
434+
if prop_format in HAP_FORMAT_NUMERICS:
380435
hap_rep.update(
381-
{
382-
k: self.properties[k]
383-
for k in PROP_NUMERIC.intersection(self.properties)
384-
}
436+
{k: properties[k] for k in PROP_NUMERIC.intersection(properties)}
385437
)
386438

387-
if PROP_VALID_VALUES in self.properties:
439+
if PROP_VALID_VALUES in properties:
388440
hap_rep[HAP_REPR_VALID_VALUES] = sorted(
389-
self.properties[PROP_VALID_VALUES].values()
441+
properties[PROP_VALID_VALUES].values()
390442
)
391-
elif self.properties[PROP_FORMAT] == HAP_FORMAT_STRING:
392-
max_length = self.properties.get(HAP_REPR_MAX_LEN, DEFAULT_MAX_LENGTH)
443+
elif prop_format == HAP_FORMAT_STRING:
444+
max_length = properties.get(HAP_REPR_MAX_LEN, DEFAULT_MAX_LENGTH)
393445
if max_length != DEFAULT_MAX_LENGTH:
394446
hap_rep[HAP_REPR_MAX_LEN] = max_length
395-
if HAP_PERMISSION_READ in self.properties[PROP_PERMISSIONS]:
396-
hap_rep[HAP_REPR_VALUE] = value
397447

448+
if include_value and HAP_PERMISSION_READ in permissions:
449+
hap_rep[HAP_REPR_VALUE] = self.get_value()
450+
451+
if not include_value:
452+
self._to_hap_cache = hap_rep
453+
elif not self.getter_callback:
454+
# Only cache if there is no getter_callback
455+
self._to_hap_cache_with_value = hap_rep
398456
return hap_rep
399457

400458
@classmethod

0 commit comments

Comments
 (0)