diff --git a/src/zeroconf/_services/info.pxd b/src/zeroconf/_services/info.pxd index 6ab77424..c53342cb 100644 --- a/src/zeroconf/_services/info.pxd +++ b/src/zeroconf/_services/info.pxd @@ -53,6 +53,7 @@ cdef class ServiceInfo(RecordUpdateListener): cdef public str server cdef public str server_key cdef public cython.dict _properties + cdef public cython.dict _decoded_properties cdef public object host_ttl cdef public object other_ttl cdef public object interface_index @@ -72,6 +73,9 @@ cdef class ServiceInfo(RecordUpdateListener): @cython.locals(length="unsigned char", index="unsigned int", key_value=bytes, key_sep_value=tuple) cdef void _unpack_text_into_properties(self) + @cython.locals(k=bytes, v=bytes) + cdef void _generate_decoded_properties(self) + @cython.locals(properties_contain_str=bint) cpdef _set_properties(self, cython.dict properties) diff --git a/src/zeroconf/_services/info.py b/src/zeroconf/_services/info.py index 1397dcec..962e76bf 100644 --- a/src/zeroconf/_services/info.py +++ b/src/zeroconf/_services/info.py @@ -143,6 +143,7 @@ class ServiceInfo(RecordUpdateListener): "server", "server_key", "_properties", + "_decoded_properties", "host_ttl", "other_ttl", "interface_index", @@ -192,6 +193,7 @@ def __init__( self.server = server if server else None self.server_key = server.lower() if server else None self._properties: Optional[Dict[bytes, Optional[bytes]]] = None + self._decoded_properties: Optional[Dict[str, Optional[str]]] = None if isinstance(properties, bytes): self._set_text(properties) else: @@ -268,6 +270,15 @@ def properties(self) -> Dict[bytes, Optional[bytes]]: assert self._properties is not None return self._properties + @property + def decoded_properties(self) -> Dict[str, Optional[str]]: + """Return properties as strings.""" + if self._decoded_properties is None: + self._generate_decoded_properties() + if TYPE_CHECKING: + assert self._decoded_properties is not None + return self._decoded_properties + def async_clear_cache(self) -> None: """Clear the cache for this service info.""" self._dns_address_cache = None @@ -384,6 +395,14 @@ def _set_text(self, text: bytes) -> None: self.text = text # Clear the properties cache self._properties = None + self._decoded_properties = None + + def _generate_decoded_properties(self) -> None: + """Generates decoded properties from the properties""" + self._decoded_properties = { + k.decode("ascii", "replace"): None if v is None else v.decode("utf-8", "replace") + for k, v in self.properties.items() + } def _unpack_text_into_properties(self) -> None: """Unpacks the text field into properties""" diff --git a/tests/test_services.py b/tests/test_services.py index 87bb6fc9..b7bebfa9 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -134,6 +134,13 @@ def update_service(self, zeroconf, type, name): assert info.properties[b'prop_true'] == b'1' assert info.properties[b'prop_false'] == b'0' + assert info.decoded_properties['prop_none'] is None + assert info.decoded_properties['prop_string'] == b'a_prop'.decode('utf-8') + assert info.decoded_properties['prop_float'] == '1.0' + assert info.decoded_properties['prop_blank'] == b'a blanked string'.decode('utf-8') + assert info.decoded_properties['prop_true'] == '1' + assert info.decoded_properties['prop_false'] == '0' + assert info.addresses == addresses[:1] # no V6 by default assert set(info.addresses_by_version(r.IPVersion.All)) == set(addresses) @@ -194,11 +201,13 @@ def update_service(self, zeroconf, type, name): info = zeroconf_browser.get_service_info(type_, registration_name) assert info is not None assert info.properties[b'prop_blank'] == properties['prop_blank'] + assert info.decoded_properties['prop_blank'] == b'an updated string'.decode('utf-8') cached_info = ServiceInfo(subtype, registration_name) cached_info.load_from_cache(zeroconf_browser) assert cached_info.properties is not None assert cached_info.properties[b'prop_blank'] == properties['prop_blank'] + assert cached_info.decoded_properties['prop_blank'] == b'an updated string'.decode('utf-8') zeroconf_registrar.unregister_service(info_service) service_removed.wait(1)