-
Notifications
You must be signed in to change notification settings - Fork 116
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
Characteristic improvements #73
Changes from 1 commit
e555607
78f6758
2505401
b25f052
9c26ce3
9269b42
4bcf00c
2dafa0f
79de11f
1d94c34
228d9f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,21 +56,14 @@ class CharacteristicError(Exception): | |
pass | ||
|
||
|
||
class NotConfiguredError(Exception): | ||
"""Raised when an operation is attempted on a characteristic that has not been | ||
fully configured. | ||
""" | ||
pass | ||
|
||
|
||
_HAP_NUMERIC_FIELDS = {"maxValue", "minValue", "minStep", "unit"} | ||
_HAP_NUMERIC_FIELDS = ('maxValue', 'minValue', 'minStep', 'unit') | ||
"""Fields that should be included in the HAP representation of the characteristic. | ||
|
||
That is, if they are present in the specification of a numeric-value characteristic. | ||
""" | ||
|
||
|
||
class Characteristic(object): | ||
class Characteristic: | ||
"""Represents a HAP characteristic, the smallest unit of the smart home. | ||
|
||
A HAP characteristic is some measurement or state, like battery status or | ||
|
@@ -79,6 +72,9 @@ class Characteristic(object): | |
like format, min and max values, valid values and others. | ||
""" | ||
|
||
__slots__ = ['display_name', 'type_id', 'properties', 'broker', | ||
'setter_callback', 'value'] | ||
|
||
def __init__(self, display_name, type_id, properties, value=None, broker=None): | ||
"""Initialise with the given properties. | ||
|
||
|
@@ -98,34 +94,66 @@ def __init__(self, display_name, type_id, properties, value=None, broker=None): | |
- else use `HAP_FORMAT.DEFAULT` for the format of this characteristic. | ||
:type value: Depends on `properties["Format"]` | ||
""" | ||
assert "Format" in properties and "Permissions" in properties | ||
if 'Format' not in properties or 'Permissions' not in properties: | ||
raise ValueError('Invalid properties') | ||
self.display_name = display_name | ||
self.type_id = type_id | ||
self.properties = properties | ||
if value: | ||
self.value = value | ||
else: | ||
if self.properties.get('ValidValues'): | ||
self.value = min(self.properties['ValidValues'].values()) | ||
else: | ||
self.value = HAP_FORMAT.DEFAULT[properties["Format"]] | ||
self.broker = broker | ||
self.setter_callback = None | ||
self.value = value or self._get_default_value() | ||
|
||
def __repr__(self): | ||
"""Return the representation of the characteristic.""" | ||
return "<characteristic display_name='{}' value={} properties={}>" \ | ||
return '<characteristic display_name={} value={} properties={}>' \ | ||
.format(self.display_name, self.value, self.properties) | ||
|
||
def _get_default_value(self): | ||
"""Helper method. Return default value for format.""" | ||
if self.properties.get('ValidValues'): | ||
return min(self.properties['ValidValues'].values()) | ||
else: | ||
value = HAP_FORMAT.DEFAULT[self.properties['Format']] | ||
return self._validate_value(value) | ||
|
||
def _validate_value(self, value): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method actually transforms the value to a valid one, i.e. it is not validation only. Can we rename to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I think this would be quite a good usecase for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know I suggested an As a user of the API you would write a value to Just wondering if instead That way, to a user it's clear that when you call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After my comment I though about this again an I would agree in many cases @thomaspurchas:
|
||
"""Perform value validation depending on format.""" | ||
if self.properties.get('ValidValues'): | ||
if value not in self.properties['ValidValues'].values(): | ||
raise ValueError | ||
else: | ||
return value | ||
elif self.properties['Format'] == HAP_FORMAT.STRING: | ||
return str(value)[:256] | ||
elif self.properties['Format'] == HAP_FORMAT.NUMERIC: | ||
value = min(self.properties.get('maxValue', value), value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not every char that is numeric has a max or min value. How about: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually that's what I should have done here, or at least intended to do (if it doesn't work).
|
||
return max(self.properties.get('minValue', value), value) | ||
elif self.properties['Format'] == HAP_FORMAT.BOOL: | ||
return bool(value) | ||
elif self.properties['Format'] == HAP_FORMAT.ARRAY: | ||
# TODO: Add validation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if we want to add validation for those cases as well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't, it will become needlessly complex. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will delete those than |
||
pass | ||
elif self.properties['Format'] == HAP_FORMAT.DICTIONARY: | ||
# TODO: Add validation | ||
pass | ||
elif self.properties['Format'] == HAP_FORMAT.DATA: | ||
# TODO: Add validation | ||
pass | ||
elif self.properties['Format'] == HAP_FORMAT.TLV8: | ||
# TODO: Add validation | ||
pass | ||
|
||
return value | ||
|
||
def set_value(self, value, should_notify=True, should_callback=True): | ||
"""Set the given raw value. It is checked if it is a valid value. | ||
"""Set the given raw value. | ||
|
||
:param value: The value to assign as this Characteristic's value. | ||
:type value: Depends on properties["Format"] | ||
|
||
:param should_notify: Whether a the change should be sent to subscribed clients. | ||
The notification is called _after_ the setter callback. Notify will be | ||
performed if and only if the broker is set, i.e. not None. | ||
The notification is called _after_ the setter callback. | ||
Notify will be performed if broker is set. | ||
:type should_notify: bool | ||
|
||
:param should_callback: Whether to invoke the callback, if such is set. This | ||
|
@@ -137,13 +165,10 @@ def set_value(self, value, should_notify=True, should_callback=True): | |
:raise ValueError: When the value being assigned is not one of the valid values | ||
for this Characteristic. | ||
""" | ||
if self.properties.get('ValidValues') and \ | ||
value not in self.properties['ValidValues'].values(): | ||
raise ValueError | ||
self.value = value | ||
if self.setter_callback is not None and should_callback: | ||
self.setter_callback(value) | ||
if should_notify and self.broker is not None: | ||
self.value = self._validate_value(value) | ||
if self.setter_callback and should_callback: | ||
self.setter_callback(self.value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the callback should just be a callback and only be called if the value is changed in HomeKit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cdce8p could you point me to an example where Also There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To expand a little more on the above. There doesn't currently appear to be away for an @ikalchev do you have any docs that explaining how client value updates are processed and forwarded onto There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I may: You have a bulb, which eventually the user taps on in iOS. This sends a request to HAP-python, which triggers the On characteristic's About the docs: I haven't documented it, sorry about that. Will update soon. In HAP-python, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ikalchev ok, if I understand this right. iOS clients cause If that's the case wouldn't providing separate APIs be a little cleaner, rather than using flags? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @thomaspurchas Client updates end up in
In both cases the user should be able to plug into
Personally, I think changing the flow in
will be flexible enough, but some input will mean a lot. Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It really depends on your use case to be honest. I had discussion with people for this before, but we couldn't figure a better way for it. For example, if your accessory wants to turn on the light, it can just call The problem is how to handle both cases above well enough. So far I have found these flags acceptable in the cases I've seen, but then what have I seen? If you think the API is missing stuff or if you have a suggestion, I would be more than happy to discuss it and make changes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will try to come up with a better way for this once more, but a use case/workflow to think about would be nice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The approach I would use is have For the I think it's very odd for people to be using this API to have the callback triggered. Presumably anything that wants to manipulate the That way there is a clear separation of responsibilities, This would also closely match how JS-HAP works as well, and prevents py-HAP from being tangled into peoples accessory code. Happy to put together some cases/workflows, and also a PR to make that happen. Thoughts from yourself and @cdce8p welcomed! |
||
if self.broker and should_notify: | ||
self.notify() | ||
|
||
def override_properties(self, properties=None, valid_values=None): | ||
|
@@ -163,43 +188,25 @@ def override_properties(self, properties=None, valid_values=None): | |
if valid_values: | ||
self.properties['ValidValues'] = valid_values | ||
|
||
def get_hap_value(self): | ||
"""Get the value of the characteristic, constrained with the HAP properties. | ||
""" | ||
val = self.value | ||
if self.properties["Format"] == HAP_FORMAT.STRING: | ||
val = val[:256] | ||
elif self.properties["Format"] in HAP_FORMAT.NUMERIC: | ||
if "maxValue" in self.properties: | ||
val = min(self.properties["maxValue"], val) | ||
if "minValue" in self.properties: | ||
val = max(self.properties["minValue"], val) | ||
return val | ||
|
||
def notify(self): | ||
"""Notify clients about a value change. | ||
"""Notify clients about a value change. Sends the value. | ||
|
||
.. note:: Non-blocking, i.e. does not wait for the update to be sent. | ||
.. note:: Uses the `get_hap_value`, i.e. sends the HAP value. | ||
.. seealso:: accessory_driver.publish | ||
|
||
:raise NotConfiguredError: When the broker is not set. | ||
:raise RuntimeError: When the broker is not set. | ||
""" | ||
if self.broker is None: | ||
raise NotConfiguredError("Attempted to notify when `broker` is None. " | ||
"Consider adding the characteristic to a " | ||
"Service and then to an Accessory.") | ||
if not self.broker: | ||
raise RuntimeError('Notify failed, because broker is not set') | ||
|
||
data = { | ||
"type_id": self.type_id, | ||
"value": self.get_hap_value(), | ||
'type_id': self.type_id, | ||
'value': self.value, | ||
} | ||
self.broker.publish(data, self) | ||
|
||
def to_HAP(self, iid_manager): | ||
"""Create a HAP representation of this Characteristic. | ||
|
||
.. note:: Uses the `get_hap_value`, i.e. sends the HAP value. | ||
"""Create a HAP representation of this Characteristic. Sends the value. | ||
|
||
:param iid_manager: IID manager to query for this object's IID. | ||
:type iid_manager: IIDManager | ||
|
@@ -221,12 +228,11 @@ def to_HAP(self, iid_manager): | |
else: | ||
value_info = dict() | ||
|
||
val = self.get_hap_value() | ||
if self.properties["Format"] == HAP_FORMAT.STRING: | ||
if len(val) > 64: | ||
value_info["maxLen"] = min(len(val), 256) | ||
if len(self.value) > 64: | ||
value_info["maxLen"] = min(len(self.value), 256) | ||
if HAP_PERMISSIONS.READ in self.properties["Permissions"]: | ||
value_info["value"] = val | ||
value_info["value"] = self.value | ||
|
||
hap_rep.update(value_info) | ||
return hap_rep |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
object
inheritance is no longer necessary