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
Zeroconf.get_service_info returns None #288
Comments
Please provide the OS you're running, Python version, python-zeroconf version the code you run and the logging output with the debug logging enabled. |
Basically I'm running zeroconf via Home Assistant in my case on a Raspian OS with docker. For debugging I used however my computer with Windows OS. I'm trying to write a zeroconf discovery for VELUX KLF200. Velux provide on their API specification on chapter "15 Appendix 3: Identifying IP address of a KLF200 device using mDNS protocol" a way to discover KLF200 on the network. https://velcdn.azureedge.net/~/media/com/api/klf200/technical%20specification%20for%20klf%20200%20api-ver3-18.pdf Trying this with Bonjour Browser also discovers this device, but using your zeroconf implementation only discovers the service but does not provide the service_info. I forked your latest zeroconf and run it on my computer with the following modified code from your README: from zeroconf import ServiceBrowser, Zeroconf, ServiceInfo
import logging
fh = logging.FileHandler("zeroconf.log")
fh.setLevel(logging.DEBUG)
ch = logging.FileHandler("zeroconf.log")
ch.setLevel(logging.WARNING)
nh = logging.StreamHandler()
nh.setLevel(logging.WARNING)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
log = logging.getLogger("zeroconf")
log.setLevel(logging.DEBUG)
log.addHandler(fh)
log.addHandler(ch)
log.addHandler(nh)
class MyListener:
def remove_service(self, zeroconf, type, name):
print("Service %s removed" % (name,))
def add_service(self, zeroconf, type, name):
log.warning("Added service: %s", name)
if name.startswith("VELUX_KLF_LAN"):
log.warning("Found VELUX_KLF_LAN device")
info = zeroconf.get_service_info(type, name, timeout=3000)
log.warning("Got service info for %s, service info: %s" % (name, info))
info = zeroconf.get_service_info(type, name, timeout=30000)
log.warning("Service %s added by using Bonjour Browser in parallel, service info: %s" % (name, info))
def update_service(self, zeroconf, type, name):
log.warning("Got update for service: %s", name)
if name.startswith("VELUX_KLF_LAN"):
log.warning("Update available for VELUX_KLF_LAN device")
info = zeroconf.get_service_info(type, name, timeout=3000)
log.warning("Service %s update is available with the following info: %s"% (name, info))
info = zeroconf.get_service_info(type, name, timeout=30000)
log.warning("Service %s updated by using Bonjour Browser in parallel, service info: %s" % (name, info))
zeroconf = Zeroconf()
listener = MyListener()
browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)
try:
input("Press enter to exit...\n\n")
finally:
zeroconf.close() Attached is the log output. In the first 3 seconds you can see that none is returned as service info. I already tried to increase but without success. Now I added a second try where I increased the timing in order to open Bonjour Browser in parallel. Now you can see that also zeroconf provides a service info. |
If I read the log correctly, this is the question from zeroconf intended to get the service info.
I assume these are the questions from Bonjour, received on both interfaces:
With answer:
Bonjour asks for out = DNSOutgoing(_FLAGS_QR_QUERY)
#out.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
#out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN), now)
#out.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
#out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN), now)
if self.server is not None:
out.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
#out.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN), now)
#out.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
#out.add_answer_at_time(
# zc.cache.get_by_details(self.server, _TYPE_AAAA, _CLASS_IN), now
#) One thing which seems a bit odd is that zeroconf opens two sets of sockets, and this is only done in the
|
No, I always disconnected Home Assistant from the Network when performing this test. Attached is the modified version. I used visual studio code to run the code, but I also included one log done by calling |
My problem is also that home assistant zeroconf component does not trigger other components if service_info is None in order to prevent the browser thread from collapsing. Otherwise I could use the name of the service to get the IP via socket.gethostbyname(hostname) where hostname is the service name without the last dot. |
Edit: Never mind what I wrote previously, I didn't realize the logs you uploaded include several iterations of running your test program. Maybe you could add something to the log when the program starts so it's possible to see where an iteration starts? Can you try to modify like this instead: out = DNSOutgoing(_FLAGS_QR_QUERY)
out.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
#out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN), now)
#out.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
#out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN), now)
if self.server is not None:
out.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
#out.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN), now)
#out.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
#out.add_answer_at_time(
# zc.cache.get_by_details(self.server, _TYPE_AAAA, _CLASS_IN), now
#)
That comments just means that there would be exceptions thrown if trying to use the returned |
This modification now works. I got immediately the service info. What does this means now? |
The difference between Avahi and zeroconf was that Avahi asked fewer questions when trying to get the service info. Since it's now working, I think it means the Velux device has a broken mDNS implementation. Can you try this also: out = DNSOutgoing(_FLAGS_QR_QUERY)
out.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN), now)
out.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN), now)
if self.server is not None:
out.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN), now)
#out.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
#out.add_answer_at_time(
# zc.cache.get_by_details(self.server, _TYPE_AAAA, _CLASS_IN), now
#) |
I checked your proposal, but it fails, I got no service info. Therefore I tried to identify which question causes this problem and figured out that as soon as I include the "_TYPE_TXT" this issue occurs. All other questions are OK. Following example provides the service info: out = DNSOutgoing(_FLAGS_QR_QUERY)
out.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN), now)
# out.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN), now)
if self.server is not None:
out.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN), now)
out.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
out.add_answer_at_time(
zc.cache.get_by_details(self.server, _TYPE_AAAA, _CLASS_IN), now
) zeroconf_with_your_last_configuration.log zeroconf_with_my_above_configuration.log Also I removed now the MyListener class as follows: class MyListener:
def remove_service(self, zeroconf, type, name):
print("Service %s removed" % (name,))
def add_service(self, zeroconf, type, name):
log.warning("Added service: %s", name)
if name.startswith("VELUX_KLF_LAN"):
log.warning("Found VELUX_KLF_LAN device")
info = zeroconf.get_service_info(type, name, timeout=3000)
log.warning("Got service info for %s, service info: %s" % (name, info))
# info = zeroconf.get_service_info(type, name, timeout=30000)
# log.warning("Service %s added by using Bonjour Browser in parallel, service info: %s" % (name, info))
def update_service(self, zeroconf, type, name):
log.warning("Got update for service: %s", name)
if name.startswith("VELUX_KLF_LAN"):
log.warning("Update available for VELUX_KLF_LAN device")
info = zeroconf.get_service_info(type, name, timeout=3000)
log.warning("Service %s update is available with the following info: %s"% (name, info))
# info = zeroconf.get_service_info(type, name, timeout=30000)
# log.warning("Service %s updated by using Bonjour Browser in parallel, service info: %s" % (name, info)) |
OK, so it seems the device only sends back an answer to the @jstasiak Could/should we avoid asking all the questions every time if we have recent answers? out = DNSOutgoing(_FLAGS_QR_QUERY)
if not zc.cache.has_recent(self.name, _TYPE_SRV, _CLASS_IN):
out.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN), now)
if not zc.cache.has_recent(self.name, _TYPE_TXT, _CLASS_IN):
out.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN), now)
if self.server is not None:
if not zc.cache.has_recent(self.name, _TYPE_A, _CLASS_IN):
out.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN), now)
if not zc.cache.has_recent(self.name, _TYPE_AAAA, _CLASS_IN):
out.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
out.add_answer_at_time(
zc.cache.get_by_details(self.server, _TYPE_AAAA, _CLASS_IN), now
) |
I have no immediate opinion on this but it's worth exploring. |
OK, I think the main reason zeroconf does not get an IP is that Velux KLF200 only responses with maximum 2 records, so if I change the sequence as below, I receive a service info but never see a TXT record. I don't know if this is a performance issue. The first response only includes one srv-record , while the second has additionally an a-record. out = DNSOutgoing(_FLAGS_QR_QUERY)
out.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN), now)
if self.server is not None:
out.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN), now)
out.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
out.add_answer_at_time(
zc.cache.get_by_details(self.server, _TYPE_AAAA, _CLASS_IN), now
)
out.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN), now)
zc.send(out)
next_ = now + delay
delay *= 2 zeroconf_with_changed_sequence.log Also that one works, but KLF200 never gives a record for _TYPE_AAAA out = DNSOutgoing(_FLAGS_QR_QUERY)
out.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN), now)
out.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN), now)
zc.send(out)
if self.server is not None:
out2 = DNSOutgoing(_FLAGS_QR_QUERY)
out2.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
out2.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN), now)
out2.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
out2.add_answer_at_time(
zc.cache.get_by_details(self.server, _TYPE_AAAA, _CLASS_IN), now
)
zc.send(out2) zeroconf_2_questions_per_frame.log For testing I also seperated all Questions: out1 = DNSOutgoing(_FLAGS_QR_QUERY)
out1.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
out1.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN), now)
zc.send(out1)
out2 = DNSOutgoing(_FLAGS_QR_QUERY)
out2.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
out2.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN), now)
zc.send(out2)
if self.server is not None:
out3 = DNSOutgoing(_FLAGS_QR_QUERY)
out3.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
out3.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN), now)
zc.send(out3)
out4 = DNSOutgoing(_FLAGS_QR_QUERY)
out4.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
out4.add_answer_at_time(
zc.cache.get_by_details(self.server, _TYPE_AAAA, _CLASS_IN), now
)
zc.send(out4) |
That's... interesting. I'm wondering what the right workaround for this on our side could be. |
Is there a possibility to skip questions in a message which are already answered? |
@jstasiak not asking questions we already know would work, right? |
Oh yeah, I would think so, yes. |
OK, any proposals how to implement? out = DNSOutgoing(_FLAGS_QR_QUERY)
if not zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN):
out.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN), now)
if not zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN):
out.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN), now)
if self.server is not None:
if not zc.cache.get_by_details(self.name, _TYPE_A, _CLASS_IN):
out.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
out.add_answer_at_time(zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN), now)
if not zc.cache.get_by_details(self.name, _TYPE_AAAA, _CLASS_IN):
out.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
out.add_answer_at_time(
zc.cache.get_by_details(self.server, _TYPE_AAAA, _CLASS_IN), now
) At least it provides reasonable results: |
@pawlizio yeah, like that, but instead of just checking if the answer is in the cache or not maybe also add a check for age if it is cached. |
I'm a pretty inexperienced with all this stuff, so it's quite hard to understand the details. How to check the age and what would be an acceptable value? |
Maybe something like this? def request(self, zc: 'Zeroconf', timeout: float) -> bool:
"""Returns true if the service could be discovered on the
network, and updates this object with details discovered.
"""
now = current_time_millis()
delay = _LISTENER_TIME
next_ = now + delay
first = next_
last = now + timeout
record_types_for_check_cache = [(_TYPE_SRV, _CLASS_IN), (_TYPE_TXT, _CLASS_IN)]
if self.server is not None:
record_types_for_check_cache.append((_TYPE_A, _CLASS_IN))
record_types_for_check_cache.append((_TYPE_AAAA, _CLASS_IN))
for record_type in record_types_for_check_cache:
cached = zc.cache.get_by_details(self.name, *record_type)
if cached:
self.update_record(zc, now, cached)
if self.server is not None and self.text is not None and self._addresses:
return True
try:
zc.add_listener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
while self.server is None or self.text is None or not self._addresses:
if last <= now:
return False
if next_ <= now:
out = DNSOutgoing(_FLAGS_QR_QUERY)
cached_entry = zc.cache.get_by_details(self.name, _TYPE_SRV, _CLASS_IN)
if not cached_entry or cached_entry.created < first:
out.add_question(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
out.add_answer_at_time(cached_entry, now)
cached_entry = zc.cache.get_by_details(self.name, _TYPE_TXT, _CLASS_IN)
if not cached_entry or cached_entry.created < first:
out.add_question(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
out.add_answer_at_time(cached_entry, now)
if self.server is not None:
cached_entry = zc.cache.get_by_details(self.server, _TYPE_A, _CLASS_IN)
if not cached_entry or cached_entry.created < first:
out.add_question(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
out.add_answer_at_time(cached_entry, now)
cached_entry = zc.cache.get_by_details(self.name, _TYPE_AAAA, _CLASS_IN)
if not cached_entry or cached_entry.created < first:
out.add_question(DNSQuestion(self.server, _TYPE_AAAA, _CLASS_IN))
out.add_answer_at_time(cached_entry, now)
zc.send(out)
next_ = now + delay
delay *= 2
zc.wait(min(next_, last) - now)
now = current_time_millis()
finally:
zc.remove_listener(self)
return True |
I have a velux KLF200 and trying to indentify its IP by it's mdns name. However using your example code does find the name of the device, but get_service_info always runs into timout.
I aready increased the timeout to 60000, but still I only return None due to timeout. The strange thing is, when using in parallel Bonjour browser get_service_info immediately returns correct information.
The text was updated successfully, but these errors were encountered: