Skip to content

Commit 8117856

Browse files
committed
Service callback for set_characteristics
For some services we want to send all the char value changes at once. This resolves an issue where we send ON and then BRIGHTNESS and the light would go to 100% and then dim to the brightness because each callback would only send one char at a time.
1 parent c8ffd07 commit 8117856

File tree

4 files changed

+75
-4
lines changed

4 files changed

+75
-4
lines changed

pyhap/accessory_driver.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454

5555
CHAR_STAT_OK = 0
5656
SERVICE_COMMUNICATION_FAILURE = -70402
57+
SERVICE_CALLBACK = 0
58+
SERVICE_CALLBACK_DATA = 1
5759

5860

5961
def callback(func):
@@ -633,6 +635,7 @@ def set_characteristics(self, chars_query, client_addr):
633635
:type chars_query: dict
634636
"""
635637
# TODO: Add support for chars that do no support notifications.
638+
service_callbacks = {}
636639
for cq in chars_query[HAP_REPR_CHARS]:
637640
aid, iid = cq[HAP_REPR_AID], cq[HAP_REPR_IID]
638641
char = self.accessory.get_characteristic(aid, iid)
@@ -647,6 +650,26 @@ def set_characteristics(self, chars_query, client_addr):
647650
if HAP_REPR_VALUE in cq:
648651
# TODO: status needs to be based on success of set_value
649652
char.client_update_value(cq[HAP_REPR_VALUE], client_addr)
653+
# For some services we want to send all the char value
654+
# changes at once. This resolves an issue where we send
655+
# ON and then BRIGHTNESS and the light would go to 100%
656+
# and then dim to the brightness because each callback
657+
# would only send one char at a time.
658+
service = char.service
659+
660+
if service and service.setter_callback:
661+
service_callbacks.setdefault(
662+
service.display_name,
663+
[service.setter_callback, {}]
664+
)
665+
service_callbacks[service.display_name][
666+
SERVICE_CALLBACK_DATA
667+
][char.display_name] = cq[HAP_REPR_VALUE]
668+
669+
for service_name in service_callbacks:
670+
service_callbacks[service_name][SERVICE_CALLBACK](
671+
service_callbacks[service_name][SERVICE_CALLBACK_DATA]
672+
)
650673

651674
def signal_handler(self, _signal, _frame):
652675
"""Stops the AccessoryDriver for a given signal.

pyhap/characteristic.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class Characteristic:
8080
"""
8181

8282
__slots__ = ('broker', 'display_name', 'properties', 'type_id',
83-
'value', 'getter_callback', 'setter_callback')
83+
'value', 'getter_callback', 'setter_callback', 'service')
8484

8585
def __init__(self, display_name, type_id, properties):
8686
"""Initialise with the given properties.
@@ -103,6 +103,7 @@ def __init__(self, display_name, type_id, properties):
103103
self.value = self._get_default_value()
104104
self.getter_callback = None
105105
self.setter_callback = None
106+
self.service = None
106107

107108
def __repr__(self):
108109
"""Return the representation of the characteristic."""

pyhap/service.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Service:
1414
"""
1515

1616
__slots__ = ('broker', 'characteristics', 'display_name', 'type_id',
17-
'linked_services', 'is_primary_service')
17+
'linked_services', 'is_primary_service', 'setter_callback')
1818

1919
def __init__(self, type_id, display_name=None):
2020
"""Initialize a new Service object."""
@@ -24,6 +24,7 @@ def __init__(self, type_id, display_name=None):
2424
self.display_name = display_name
2525
self.type_id = type_id
2626
self.is_primary_service = None
27+
self.setter_callback = None
2728

2829
def __repr__(self):
2930
"""Return the representation of the service."""
@@ -43,6 +44,7 @@ def add_characteristic(self, *chars):
4344
for char in chars:
4445
if not any(char.type_id == original_char.type_id
4546
for original_char in self.characteristics):
47+
char.service = self
4648
self.characteristics.append(char)
4749

4850
def get_characteristic(self, name):

tests/test_accessory_driver.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
"""Tests for pyhap.accessory_driver."""
22
import tempfile
3-
from unittest.mock import patch
3+
from unittest.mock import MagicMock, patch
4+
from uuid import uuid1
45

56
import pytest
67

7-
from pyhap.accessory import Accessory, STANDALONE_AID
8+
from pyhap.accessory import STANDALONE_AID, Accessory
89
from pyhap.accessory_driver import AccessoryDriver
10+
from pyhap.characteristic import (HAP_FORMAT_INT, HAP_PERMISSION_READ,
11+
PROP_FORMAT, PROP_PERMISSIONS,
12+
Characteristic)
13+
from pyhap.const import HAP_REPR_IID, HAP_REPR_CHARS, HAP_REPR_AID, HAP_REPR_VALUE
14+
from pyhap.service import Service
15+
16+
CHAR_PROPS = {
17+
PROP_FORMAT: HAP_FORMAT_INT,
18+
PROP_PERMISSIONS: HAP_PERMISSION_READ,
19+
}
920

1021

1122
@pytest.fixture
@@ -43,6 +54,40 @@ def test_persist_load():
4354
assert driver.state.public_key == pk
4455

4556

57+
def test_service_callbacks(driver):
58+
acc = Accessory(driver, 'TestAcc')
59+
60+
service = Service(uuid1(), 'Lightbulb')
61+
char_on = Characteristic('On', uuid1(), CHAR_PROPS)
62+
char_brightness = Characteristic('Brightness', uuid1(), CHAR_PROPS)
63+
64+
service.add_characteristic(char_on)
65+
service.add_characteristic(char_brightness)
66+
67+
mock_callback = MagicMock()
68+
service.setter_callback = mock_callback
69+
70+
acc.add_service(service)
71+
driver.add_accessory(acc)
72+
73+
char_on_iid = char_on.to_HAP()[HAP_REPR_IID]
74+
char_brightness_iid = char_brightness.to_HAP()[HAP_REPR_IID]
75+
76+
driver.set_characteristics({
77+
HAP_REPR_CHARS: [{
78+
HAP_REPR_AID: acc.aid,
79+
HAP_REPR_IID: char_on_iid,
80+
HAP_REPR_VALUE: True
81+
}, {
82+
HAP_REPR_AID: acc.aid,
83+
HAP_REPR_IID: char_brightness_iid,
84+
HAP_REPR_VALUE: 88
85+
}]
86+
}, "mock_addr")
87+
88+
mock_callback.assert_called_with({'On': True, 'Brightness': 88})
89+
90+
4691
def test_start_stop_sync_acc(driver):
4792
class Acc(Accessory):
4893
running = True

0 commit comments

Comments
 (0)