Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added getter_callback to Characteristic #90

Merged
merged 15 commits into from
May 9, 2018
25 changes: 20 additions & 5 deletions pyhap/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class Characteristic:
"""

__slots__ = ('display_name', 'type_id', 'properties', 'broker',
'setter_callback', 'value')
'value', 'getter_callback', 'setter_callback')

def __init__(self, display_name, type_id, properties):
"""Initialise with the given properties.
Expand All @@ -98,8 +98,9 @@ def __init__(self, display_name, type_id, properties):
self.type_id = type_id
self.properties = properties
self.broker = None
self.setter_callback = None
self.value = self._get_default_value()
self.getter_callback = None
self.setter_callback = None

def __repr__(self):
"""Return the representation of the characteristic."""
Expand All @@ -114,6 +115,11 @@ def _get_default_value(self):
value = HAP_FORMAT_DEFAULTS[self.properties[PROP_FORMAT]]
return self.to_valid_value(value)

def get_value(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation stating that this calls the getter if any to update the .value

if self.getter_callback:
self.value = self.to_valid_value(value=self.getter_callback())
return self.value

def to_valid_value(self, value):
"""Perform validation and conversion to valid value"""
if self.properties.get(PROP_VALID_VALUES):
Expand Down Expand Up @@ -168,6 +174,12 @@ def set_value(self, value, should_notify=True):
If not set_value will be aborted and an error message will be
displayed.

`Characteristic.setter_callback`
You may also define a `setter_callback` on the `Characteristic`.
This will be called with the value being set as the arg.

.. seealso:: Characteristic.value

:param value: The value to assign as this Characteristic's value.
:type value: Depends on properties["Format"]

Expand All @@ -184,12 +196,15 @@ def set_value(self, value, should_notify=True):
def client_update_value(self, value):
"""Called from broker for value change in Home app.

Change self.value to value and call callback.
Call set_value and call callback.
"""
logger.debug('client_update_value: %s to %s',
self.display_name, value)
logger.debug('%s: Client update value to %s',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the log statement in one of the last merged PR #99

self.display_name, value)
# Set new value before anything else, then notify client

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the comment below shouldn't be necessary.

self.value = value
self.notify()
# Call setter_callback
Copy link
Contributor

@cdce8p cdce8p Apr 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same here. It would be quite confusion if a client_update_value call cause a set_value log output as well.
Instead use:

        self._value = value
        self.notify()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please revert the changes to client_update_value? I don't think they are necessary for get_value.

if self.setter_callback:
self.setter_callback(value)

Expand Down
4 changes: 3 additions & 1 deletion pyhap/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def get_characteristic(self, name):
raise ValueError('Characteristic not found')

def configure_char(self, char_name, properties=None, valid_values=None,
value=None, setter_callback=None):
value=None, setter_callback=None, getter_callback=None):
"""Helper method to return fully configured characteristic."""
char = self.get_characteristic(char_name)
if properties or valid_values:
Expand All @@ -59,6 +59,8 @@ def configure_char(self, char_name, properties=None, valid_values=None,
char.set_value(value, should_notify=False)
if setter_callback:
char.setter_callback = setter_callback
if getter_callback:
char.getter_callback = getter_callback
return char

def to_HAP(self):
Expand Down
159 changes: 159 additions & 0 deletions tests/test_characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,162 @@ def test_from_dict():
assert char.display_name == 'Test Char'
assert char.type_id == uuid
assert char.properties == {'Format': 'int', 'Permissions': 'read'}

class TestCharacteristic(unittest.TestCase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All tests in the TestCharacteristic class got changed in #99 as well. They are now outside, so you should be able to remove the class.


def test_repr(self):
char = get_char(PROPERTIES.copy())
del char.properties['Permissions']
self.assertEqual(
'<characteristic display_name=Test Char value=0 ' \
'properties={\'Format\': \'int\'}>', char.__repr__())

def test_default_value(self):
char = get_char(PROPERTIES.copy())
self.assertEqual(char.value, HAP_FORMAT_DEFAULTS[PROPERTIES['Format']])

def test_get_default_value(self):
valid_values = {'foo': 2, 'bar': 3}
char = get_char(PROPERTIES.copy(), valid=valid_values)
self.assertEqual(char.value, 2)
self.assertIn(char.value, valid_values.values())
char = get_char(PROPERTIES.copy(), min_value=3, max_value=10)
self.assertEqual(char.value, 3)

def test_to_valid_value(self):
char = get_char(PROPERTIES.copy(), valid={'foo': 2, 'bar': 3},
min_value=2, max_value=7)
with self.assertRaises(ValueError):
char.to_valid_value(1)
self.assertEqual(char.to_valid_value(2), 2)

del char.properties['ValidValues']
for value in ('2', None):
with self.assertRaises(ValueError):
char.to_valid_value(value)
self.assertEqual(char.to_valid_value(1), 2)
self.assertEqual(char.to_valid_value(5), 5)
self.assertEqual(char.to_valid_value(8), 7)

char.properties['Format'] = 'string'
self.assertEqual(char.to_valid_value(24), '24')

char.properties['Format'] = 'bool'
self.assertTrue(char.to_valid_value(1))
self.assertFalse(char.to_valid_value(0))

char.properties['Format'] = 'dictionary'
self.assertEqual(char.to_valid_value({'a': 1}), {'a': 1})

def test_override_properties_properties(self):
new_properties = {'minValue': 10, 'maxValue': 20, 'step': 1}
char = get_char(PROPERTIES.copy(), min_value=0, max_value=1)
char.override_properties(properties=new_properties)
self.assertEqual(char.properties['minValue'],
new_properties['minValue'])
self.assertEqual(char.properties['maxValue'],
new_properties['maxValue'])
self.assertEqual(char.properties['step'], new_properties['step'])

def test_override_properties_valid_values(self):
new_valid_values = {'foo2': 2, 'bar2': 3}
char = get_char(PROPERTIES.copy(), valid={'foo': 1, 'bar': 2})
char.override_properties(valid_values=new_valid_values)
self.assertEqual(char.properties['ValidValues'], new_valid_values)

def test_override_properties_error(self):
char = get_char(PROPERTIES.copy())
with self.assertRaises(ValueError):
char.override_properties()

def test_set_value(self):
path = 'pyhap.characteristic.Characteristic.notify'
char = get_char(PROPERTIES.copy(), min_value=3, max_value=7)

with patch(path) as mock_notify:
char.set_value(5)
self.assertEqual(char.value, 5)
self.assertFalse(mock_notify.called)

char.broker = Mock()
char.set_value(8, should_notify=False)
self.assertEqual(char.value, 7)
self.assertFalse(mock_notify.called)

char.set_value(1)
self.assertEqual(char.value, 3)
self.assertEqual(mock_notify.call_count, 1)

def test_client_update_value(self):
path_notify = 'pyhap.characteristic.Characteristic.notify'
char = get_char(PROPERTIES.copy())

with patch(path_notify) as mock_notify:
char.client_update_value(4)
self.assertEqual(char.value, 4)
with patch.object(char, 'setter_callback') as mock_callback:
char.client_update_value(3)

self.assertEqual(char.value, 3)
self.assertEqual(mock_notify.call_count, 2)
mock_callback.assert_called_with(3)

def test_notify(self):
char = get_char(PROPERTIES.copy())

char.value = 2
with self.assertRaises(AttributeError):
char.notify()

with patch.object(char, 'broker') as mock_broker:
char.notify()
mock_broker.publish.assert_called_with(2, char)

def test_to_HAP_numberic(self):
char = get_char(PROPERTIES.copy(), min_value=1, max_value=2)
with patch.object(char, 'broker') as mock_broker:
mock_iid = mock_broker.iid_manager.get_iid
mock_iid.return_value = 2
hap_repr = char.to_HAP()
mock_iid.assert_called_with(char)

self.assertEqual(
hap_repr,
{
'iid': 2,
'type': ANY,
'description': 'Test Char',
'perms': ['pr'],
'format': 'int',
'maxValue': 2,
'minValue': 1,
'value': 1,
})

def test_to_HAP_string(self):
char = get_char(PROPERTIES.copy())
char.properties['Format'] = 'string'
char.value = 'aaa'
with patch.object(char, 'broker') as mock_broker:
hap_repr = char.to_HAP()
self.assertEqual(hap_repr['format'], 'string')
self.assertNotIn('maxLen', hap_repr)

char.value = 'aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee' \
'ffffffffffgggggggggg'
with patch.object(char, 'broker') as mock_broker:
hap_repr = char.to_HAP()
self.assertEqual(hap_repr['maxLen'], 70)
self.assertEqual(hap_repr['value'], char.value)

def test_to_HAP_bool(self):
char = get_char(PROPERTIES.copy())
char.properties['Format'] = 'bool'
with patch.object(char, 'broker') as mock_broker:
hap_repr = char.to_HAP()
self.assertEqual(hap_repr['format'], 'bool')

char.properties['Permissions'] = []
with patch.object(char, 'broker') as mock_broker:
hap_repr = char.to_HAP()
self.assertNotIn('value', hap_repr)