-
Notifications
You must be signed in to change notification settings - Fork 119
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 2 commits
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 |
---|---|---|
|
@@ -4,6 +4,8 @@ | |
A Characteristic is the smallest unit of the smart home, e.g. | ||
a temperature measuring or a device status. | ||
""" | ||
import uuid | ||
|
||
|
||
class HAP_FORMAT: | ||
BOOL = 'bool' | ||
|
@@ -56,21 +58,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: | ||
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.
|
||
"""Represents a HAP characteristic, the smallest unit of the smart home. | ||
|
||
A HAP characteristic is some measurement or state, like battery status or | ||
|
@@ -79,7 +74,10 @@ class Characteristic(object): | |
like format, min and max values, valid values and others. | ||
""" | ||
|
||
def __init__(self, display_name, type_id, properties, value=None, broker=None): | ||
__slots__ = ('display_name', 'type_id', 'properties', 'broker', | ||
'setter_callback', 'value') | ||
|
||
def __init__(self, display_name, type_id, properties): | ||
"""Initialise with the given properties. | ||
|
||
:param display_name: Name that will be displayed for this characteristic, i.e. | ||
|
@@ -91,60 +89,49 @@ def __init__(self, display_name, type_id, properties, value=None, broker=None): | |
|
||
:param properties: A dict of properties, such as Format, ValidValues, etc. | ||
:type properties: dict | ||
|
||
:param value: The initial value to set to this characteristic. If no value is given, | ||
the assigned value happens as: | ||
- if there is a ValidValue property, use some value from it. | ||
- 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 | ||
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.broker = None | ||
self.setter_callback = None | ||
self.value = 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 set_value(self, value, should_notify=True, should_callback=True): | ||
"""Set the given raw value. It is checked if it is a valid value. | ||
|
||
:param value: The value to assign as this Characteristic's value. | ||
:type value: Depends on properties["Format"] | ||
def set_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. I am still thinking about the 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 though about this case as well. It's a trade of here. I think that e.g. 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. Yeah adding a 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 guess it wont hurt anyone with |
||
"""Called from user to indicate value change. | ||
|
||
: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. | ||
:type should_notify: bool | ||
|
||
:param should_callback: Whether to invoke the callback, if such is set. This | ||
is useful in cases where you and HAP clients can both update the value and | ||
you don't want your callback called when you set the value, but want it | ||
called when clients do. Defaults to True. | ||
:type should_callback: bool | ||
|
||
:raise ValueError: When the value being assigned is not one of the valid values | ||
for this Characteristic. | ||
Will notify broker that publishes change to clients. | ||
""" | ||
if self.properties.get('ValidValues') and \ | ||
value not in self.properties['ValidValues'].values(): | ||
raise ValueError | ||
value = self.to_valid_value(value) | ||
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.notify() | ||
self.notify() | ||
|
||
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.to_valid_value(value) | ||
|
||
def to_valid_value(self, value): | ||
"""Perform validation and conversion to valid value""" | ||
if self.properties.get('ValidValues'): | ||
if value not in self.properties['ValidValues'].values(): | ||
raise ValueError('%s is not a valid value'.format(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. @ikalchev I was thinking about this. What would you think about an error log statement instead of an ValueError? The log statement would be easier to debug. 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. Why do you think so? 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. With errors you get the whole traceback and it might seem unintentional. Whereas with error log message this would be handled much cleaner. I started thinking in which cases you would use an As an implementation for us: I thought 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. 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. I think that breaks HomeKit. I tried it some time ago and it seemed to cause issues. I believe if the value doesn't get updated, that should be hint enough to think something is wrong here. 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. Why would you not want an Exception and traceback? If the value is invalid then the caller should capture it, and either log it or ignore it (py-HAP is just providing an API after all, not then entire application. The demo server could be updated to capture and log the exception). If the iOS client makes a call and sets an invalid value, then the exception should also be raised, caught by 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 think we should had over the responsibility for that. Especially because we would need a try block for every For |
||
elif self.properties['Format'] == HAP_FORMAT.STRING: | ||
value = str(value)[:256] | ||
elif self.properties['Format'] == HAP_FORMAT.BOOL: | ||
value = bool(value) | ||
elif self.properties['Format'] in HAP_FORMAT.NUMERIC: | ||
value = min(self.properties.get('maxValue', value), value) | ||
value = max(self.properties.get('minValue', value), value) | ||
return value | ||
|
||
def override_properties(self, properties=None, valid_values=None): | ||
"""Override characteristic property values and valid values. | ||
|
@@ -163,70 +150,45 @@ 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. | ||
|
||
.. 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. | ||
""" | ||
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.") | ||
|
||
data = { | ||
"type_id": self.type_id, | ||
"value": self.get_hap_value(), | ||
"""Notify clients about a value change. Sends the value.""" | ||
self.broker.publish(self.value, self) | ||
|
||
def to_dict(self): | ||
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 sure about this. See comment in PR. 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 would also prefer 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 disagree. It might just be an implementation detail, but that exactly makes it even better.
Why not? If someone whats to use that say for debugging, why should we limit that?
I get that, but what prevents us to changing that back in the future? I believe that 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. You can use def to_dict(self):
return self.to_HAP() In this way, we get best of both worlds. What is more, when/if 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. Still not convinced by this. I agree with @ikalchev, either Either way 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 leave it at def to_dict(self):
return self.to_HAP() That would add just another method to do the same think and would defeat the propose of this change (to reduce complexity). I didn't thought that would be so controversial 😅 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 nit pick, I think it actually makes more sense to do it the other way around. 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. What one developer can do in one month, two developers can do in two months. 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.
Probably better, yes 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. For a representation / debugging: |
||
"""Create a dict repr of this object for json serialization.""" | ||
json_dict = { | ||
'iid': self.broker.iid_manager.get_iid(self), | ||
'type': str(self.type_id).upper(), | ||
'description': self.display_name, | ||
'perms': self.properties['Permissions'], | ||
'format': self.properties['Format'], | ||
} | ||
self.broker.publish(data, self) | ||
|
||
def to_HAP(self, iid_manager): | ||
"""Create a HAP representation of this Characteristic. | ||
if self.properties['Format'] in HAP_FORMAT.NUMERIC: | ||
json_dict.update({k: self.properties[k] for k in | ||
self.properties.keys() & _HAP_NUMERIC_FIELDS}) | ||
elif self.properties['Format'] == HAP_FORMAT.STRING: | ||
if len(self.value) > 64: | ||
json_dict['maxLen'] = min(len(self.value), 256) | ||
if HAP_PERMISSIONS.READ in self.properties['Permissions']: | ||
json_dict['value'] = self.value | ||
|
||
.. note:: Uses the `get_hap_value`, i.e. sends the HAP value. | ||
return json_dict | ||
|
||
:param iid_manager: IID manager to query for this object's IID. | ||
:type iid_manager: IIDManager | ||
def update_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. 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. Sounds good 👍 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. Could I suggest 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. What about |
||
"""Called from broker for value change in Home app. | ||
|
||
:return: A HAP representation. | ||
:rtype: dict | ||
Change self.value to value and call callback. | ||
The callback method must return a boolean that indicates if successful. | ||
""" | ||
hap_rep = { | ||
"iid": iid_manager.get_iid(self), | ||
"type": str(self.type_id).upper(), | ||
"description": self.display_name, | ||
"perms": self.properties["Permissions"], | ||
"format": self.properties["Format"], | ||
} | ||
|
||
if self.properties["Format"] in HAP_FORMAT.NUMERIC: | ||
value_info = {k: self.properties[k] for k in | ||
self.properties.keys() & _HAP_NUMERIC_FIELDS} | ||
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 HAP_PERMISSIONS.READ in self.properties["Permissions"]: | ||
value_info["value"] = val | ||
|
||
hap_rep.update(value_info) | ||
return hap_rep | ||
self.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. Value should only be set after the |
||
self.notify() | ||
if self.setter_callback: | ||
return self.setter_callback(value) | ||
return True | ||
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. Rather than returning We can then map the exceptions to the HAP protocol status values. Using 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 would think that 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 would point out that 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 just looked at Apple's spec - there are 11 error codes (which I need to add btw). If we use exceptions, we will force the Driver from understanding these exceptions and mapping them to error codes - in this case, why not 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. There might be 11 error codes, but only some would be valid for a callback i.e. The rest are errors that should be managed by py-HAP. i.e. Additionally exceptions can be nested so the following would work: class AccessoryError(Exception):
pass
class AccessoryBusyError(AccessoryError):
pass
try:
throw AccessoryBusyError
except AccessoryError as e:
print(e) 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. Sounds good :) 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 still doesn't feel pythonic. We would then need a bunch of static vars somewhere to match the docs, or rely on users parsing the docs. Not sure how best to move this convo into another PR. 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 maybe continue here: #75 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. Let's think on that, it's not such a big deal, whatever we chose. BTW there is a discord chat, but I haven't mentioned that here because it would make things more hard to follow. 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've copied a summary of this thread into there. |
||
|
||
@classmethod | ||
def from_dict(cls, name, json_dict): | ||
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. When do you imagine this will be used? 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 plan to use it for the |
||
"""Convert json dictionary to characteristic instance.""" | ||
return cls(name, type_id=uuid.UUID(json_dict.pop('UUID')), | ||
properties=json_dict) |
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.
I though that accessories should be calling
set_value
, and in the futurevalue
will be made read only with a@property
getter.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.
I didn't mentioned that above, but I realized that there are cases in which setting the value on its one does make sense. Testing as well as the one above.
value = 0
is basically equivalent toset_value(0, should_notify=False)
if no callback has been added and the char doesn't belong to any service / accessory yet.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.
Yes, but doing this will ignore all the validation logic, which still needs to be run at some point. This could allow an invalid value to be added using
__init__
and then sent to an iOS client during aget
.Better to just use
set_value
, and have the char figure out if sending a notification makes sense (if there is no broker, then just don't bother with the notification).