Skip to content

Commit

Permalink
Merge pull request #381 from ikalchev/v4.2.0
Browse files Browse the repository at this point in the history
V4.2.0
  • Loading branch information
ikalchev authored Sep 5, 2021
2 parents 6e88070 + 492fcef commit b4343fa
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 13 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ Sections
### Developers
-->

## [4.2.0] - 2021-09-04

### Changed
- Bump zeroconf to 0.36.2. [#380](https://github.com/ikalchev/HAP-python/pull/380)

### Fixed
- Handle additional cases of invalid values. [#378](https://github.com/ikalchev/HAP-python/pull/378)

### Added
- Allow passing the zeroconf server name when creating the AccessoryDriver. [#379](https://github.com/ikalchev/HAP-python/pull/379)

## [4.1.0] - 2021-08-22

### Added
Expand Down
20 changes: 15 additions & 5 deletions pyhap/accessory_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def _wrap_service_setter(service, chars, client_addr):
class AccessoryMDNSServiceInfo(ServiceInfo):
"""A mDNS service info representation of an accessory."""

def __init__(self, accessory, state):
def __init__(self, accessory, state, zeroconf_server=None):
self.accessory = accessory
self.state = state

Expand All @@ -131,7 +131,7 @@ def __init__(self, accessory, state):
self.state.mac[-8:].replace(":", ""),
HAP_SERVICE_TYPE,
)
server = "{}-{}.{}".format(
server = zeroconf_server or "{}-{}.{}".format(
self._valid_host_name(),
self.state.mac[-8:].replace(":", ""),
"local.",
Expand Down Expand Up @@ -212,7 +212,8 @@ def __init__(
listen_address=None,
advertised_address=None,
interface_choice=None,
async_zeroconf_instance=None
async_zeroconf_instance=None,
zeroconf_server=None
):
"""
Initialize a new AccessoryDriver object.
Expand Down Expand Up @@ -259,6 +260,10 @@ def __init__(
:param async_zeroconf_instance: An AsyncZeroconf instance. When running multiple accessories or
bridges a single zeroconf instance can be shared to avoid the overhead
of processing the same data multiple times.
:param zeroconf_server: The server name that will be used for the zeroconf
ServiceInfo.
:type zeroconf_server: str
"""
if loop is None:
if sys.platform == "win32":
Expand All @@ -281,6 +286,7 @@ def __init__(

self.accessory = None
self.advertiser = async_zeroconf_instance
self.zeroconf_server = zeroconf_server
self.interface_choice = interface_choice

self.persist_file = os.path.expanduser(persist_file)
Expand Down Expand Up @@ -384,7 +390,9 @@ async def async_start(self):

# Advertise the accessory as a mDNS service.
logger.debug("Starting mDNS.")
self.mdns_service_info = AccessoryMDNSServiceInfo(self.accessory, self.state)
self.mdns_service_info = AccessoryMDNSServiceInfo(
self.accessory, self.state, self.zeroconf_server
)

if not self.advertiser:
zc_args = {}
Expand Down Expand Up @@ -609,7 +617,9 @@ def update_advertisement(self):
def async_update_advertisement(self):
"""Updates the mDNS service info for the accessory from the event loop."""
logger.debug("Updating mDNS advertisement")
self.mdns_service_info = AccessoryMDNSServiceInfo(self.accessory, self.state)
self.mdns_service_info = AccessoryMDNSServiceInfo(
self.accessory, self.state, self.zeroconf_server
)
asyncio.ensure_future(
self.advertiser.async_update_service(self.mdns_service_info)
)
Expand Down
26 changes: 23 additions & 3 deletions pyhap/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
HAP_FORMAT_UINT64,
}

DEFAULT_MAX_LENGTH = 64
ABSOLUTE_MAX_LENGTH = 256

# ### HAP Units ###
HAP_UNIT_ARC_DEGREE = "arcdegrees"
HAP_UNIT_CELSIUS = "celsius"
Expand Down Expand Up @@ -100,6 +103,15 @@ class CharacteristicError(Exception):
"""Generic exception class for characteristic errors."""


def _validate_properties(properties):
"""Throw an exception on invalid properties."""
if (
HAP_REPR_MAX_LEN in properties
and properties[HAP_REPR_MAX_LEN] > ABSOLUTE_MAX_LENGTH
):
raise ValueError(f"{HAP_REPR_MAX_LEN} may not exceed {ABSOLUTE_MAX_LENGTH}")


class Characteristic:
"""Represents a HAP characteristic, the smallest unit of the smart home.
Expand Down Expand Up @@ -136,6 +148,7 @@ def __init__(self, display_name, type_id, properties):
ValidValues, etc.
:type properties: dict
"""
_validate_properties(properties)
self.broker = None
self.display_name = display_name
self.properties = properties
Expand Down Expand Up @@ -184,7 +197,9 @@ def to_valid_value(self, value):
logger.error(error_msg)
raise ValueError(error_msg)
elif self.properties[PROP_FORMAT] == HAP_FORMAT_STRING:
value = str(value)[:256]
value = str(value)[
: self.properties.get(HAP_REPR_MAX_LEN, DEFAULT_MAX_LENGTH)
]
elif self.properties[PROP_FORMAT] == HAP_FORMAT_BOOL:
value = bool(value)
elif self.properties[PROP_FORMAT] in HAP_FORMAT_NUMERICS:
Expand All @@ -194,6 +209,9 @@ def to_valid_value(self, value):
)
logger.error(error_msg)
raise ValueError(error_msg)
min_step = self.properties.get(PROP_MIN_STEP)
if min_step:
value = value - (value % min_step)
value = min(self.properties.get(PROP_MAX_VALUE, value), value)
value = max(self.properties.get(PROP_MIN_VALUE, value), value)
if self.properties[PROP_FORMAT] != HAP_FORMAT_FLOAT:
Expand All @@ -215,6 +233,7 @@ def override_properties(self, properties=None, valid_values=None):
raise ValueError("No properties or valid_values specified to override.")

if properties:
_validate_properties(properties)
self.properties.update(properties)

if valid_values:
Expand Down Expand Up @@ -322,8 +341,9 @@ def to_HAP(self):
self.properties[PROP_VALID_VALUES].values()
)
elif self.properties[PROP_FORMAT] == HAP_FORMAT_STRING:
if len(value) > 64:
hap_rep[HAP_REPR_MAX_LEN] = min(len(value), 256)
max_length = self.properties.get(HAP_REPR_MAX_LEN, DEFAULT_MAX_LENGTH)
if max_length != DEFAULT_MAX_LENGTH:
hap_rep[HAP_REPR_MAX_LEN] = max_length
if HAP_PERMISSION_READ in self.properties[PROP_PERMISSIONS]:
hap_rep[HAP_REPR_VALUE] = value

Expand Down
2 changes: 1 addition & 1 deletion pyhap/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""This module contains constants used by other modules."""
MAJOR_VERSION = 4
MINOR_VERSION = 1
MINOR_VERSION = 2
PATCH_VERSION = 0
__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION)
__version__ = "{}.{}".format(__short_version__, PATCH_VERSION)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
README = f.read()


REQUIRES = ["cryptography", "zeroconf>=0.32.0", "h11"]
REQUIRES = ["cryptography", "zeroconf>=0.36.2", "h11"]


setup(
Expand Down
29 changes: 29 additions & 0 deletions tests/test_accessory_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,35 @@ def test_mdns_service_info(driver):
}


def test_mdns_service_info_with_specified_server(driver):
"""Test accessory mdns advert when the server is specified."""
acc = Accessory(driver, "Test Accessory")
driver.add_accessory(acc)
addr = "172.0.0.1"
mac = "00:00:00:00:00:00"
pin = b"123-45-678"
port = 11111
state = State(address=addr, mac=mac, pincode=pin, port=port)
state.setup_id = "abc"
mdns_info = AccessoryMDNSServiceInfo(acc, state, "hap1.local.")
assert mdns_info.type == "_hap._tcp.local."
assert mdns_info.name == "Test Accessory 000000._hap._tcp.local."
assert mdns_info.server == "hap1.local."
assert mdns_info.port == port
assert mdns_info.addresses == [b"\xac\x00\x00\x01"]
assert mdns_info.properties == {
"md": "Test Accessory",
"pv": "1.1",
"id": "00:00:00:00:00:00",
"c#": "1",
"s#": "1",
"ff": "0",
"ci": "1",
"sf": "1",
"sh": "+KjpzQ==",
}


@pytest.mark.parametrize(
"accessory_name, mdns_name, mdns_server",
[
Expand Down
67 changes: 64 additions & 3 deletions tests/test_characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ def test_override_properties_properties():
assert char.properties["step"] == new_properties["step"]


def test_override_properties_exceed_max_length():
"""Test if overriding the properties with invalid values throws."""
new_properties = {"minValue": 10, "maxValue": 20, "step": 1, "maxLen": 5000}
char = get_char(PROPERTIES.copy(), min_value=0, max_value=1)
with pytest.raises(ValueError):
char.override_properties(properties=new_properties)


def test_override_properties_valid_values():
"""Test if overriding the properties works for valid values."""
new_valid_values = {"foo2": 2, "bar2": 3}
Expand All @@ -119,6 +127,38 @@ def test_override_properties_error():
char.override_properties()


@pytest.mark.parametrize("int_format", HAP_FORMAT_INTS)
def test_set_value_invalid_min_step(int_format):
"""Test setting the value of a characteristic that is outside the minStep."""
path = "pyhap.characteristic.Characteristic.notify"
props = PROPERTIES.copy()
props["Format"] = int_format
props["minStep"] = 2
char = get_char(props, min_value=4, max_value=8)

with patch(path) as mock_notify:
char.set_value(5.55)
# Ensure floating point is dropped on an int property
# Ensure value is lowered to match minStep
assert char.value == 4
assert mock_notify.called is False

char.broker = Mock()
char.set_value(8, should_notify=False)
assert char.value == 8
assert mock_notify.called is False

char.set_value(1)
# Ensure value is raised to meet minValue
assert char.value == 4
assert mock_notify.call_count == 1

# No change should not generate another notify
char.set_value(4)
assert char.value == 4
assert mock_notify.call_count == 1


@pytest.mark.parametrize("int_format", HAP_FORMAT_INTS)
def test_set_value_int(int_format):
"""Test setting the value of a characteristic."""
Expand Down Expand Up @@ -322,13 +362,34 @@ def test_to_HAP_string():
assert hap_repr["format"] == "string"
assert "maxLen" not in hap_repr

char.value = (
char.set_value(
"aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffgggggggggg"
)
with patch.object(char, "broker"):
hap_repr = char.to_HAP()
assert hap_repr["maxLen"] == 70
assert hap_repr["value"] == char.value
assert "maxLen" not in hap_repr
assert hap_repr["value"] == char.value[:64]


def test_to_HAP_string_max_length_override():
"""Test created HAP representation for strings."""
char = get_char(PROPERTIES.copy())
char.properties["Format"] = "string"
char.properties["maxLen"] = 256
char.value = "aaa"
with patch.object(char, "broker"):
hap_repr = char.to_HAP()
assert hap_repr["format"] == "string"
assert "maxLen" in hap_repr
longer_than_sixty_four = (
"aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffgggggggggg"
)

char.set_value(longer_than_sixty_four)
with patch.object(char, "broker"):
hap_repr = char.to_HAP()
assert hap_repr["maxLen"] == 256
assert hap_repr["value"] == longer_than_sixty_four


def test_to_HAP_bool():
Expand Down

0 comments on commit b4343fa

Please sign in to comment.