2019-02-10 16:57:34 +01:00
|
|
|
import socket
|
|
|
|
import threading
|
|
|
|
import time
|
2021-09-06 04:48:28 +02:00
|
|
|
from typing import Dict, Optional
|
2019-02-10 16:57:34 +01:00
|
|
|
|
2021-09-05 22:22:15 +02:00
|
|
|
from zeroconf import (
|
|
|
|
_CLASS_IN,
|
|
|
|
_FLAGS_QR_QUERY,
|
|
|
|
_TYPE_A,
|
|
|
|
DNSAddress,
|
|
|
|
DNSOutgoing,
|
|
|
|
DNSRecord,
|
|
|
|
DNSQuestion,
|
|
|
|
RecordUpdateListener,
|
|
|
|
Zeroconf,
|
|
|
|
)
|
2019-02-10 16:57:34 +01:00
|
|
|
|
|
|
|
|
|
|
|
class HostResolver(RecordUpdateListener):
|
2021-09-05 22:22:15 +02:00
|
|
|
def __init__(self, name: str):
|
2019-02-10 16:57:34 +01:00
|
|
|
self.name = name
|
2021-09-05 22:22:15 +02:00
|
|
|
self.address: Optional[bytes] = None
|
2019-02-10 16:57:34 +01:00
|
|
|
|
2021-09-05 22:22:15 +02:00
|
|
|
def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
|
2019-02-10 16:57:34 +01:00
|
|
|
if record is None:
|
|
|
|
return
|
|
|
|
if record.type == _TYPE_A:
|
|
|
|
assert isinstance(record, DNSAddress)
|
|
|
|
if record.name == self.name:
|
|
|
|
self.address = record.address
|
|
|
|
|
2021-09-05 22:22:15 +02:00
|
|
|
def request(self, zc: Zeroconf, timeout: float) -> bool:
|
2019-02-10 16:57:34 +01:00
|
|
|
now = time.time()
|
|
|
|
delay = 0.2
|
|
|
|
next_ = now + delay
|
|
|
|
last = now + timeout
|
|
|
|
|
|
|
|
try:
|
2021-09-05 22:22:15 +02:00
|
|
|
zc.add_listener(self, None)
|
2019-02-10 16:57:34 +01:00
|
|
|
while self.address is None:
|
|
|
|
if last <= now:
|
|
|
|
# Timeout
|
|
|
|
return False
|
|
|
|
if next_ <= now:
|
|
|
|
out = DNSOutgoing(_FLAGS_QR_QUERY)
|
2021-03-07 20:03:16 +01:00
|
|
|
out.add_question(DNSQuestion(self.name, _TYPE_A, _CLASS_IN))
|
2019-02-10 16:57:34 +01:00
|
|
|
zc.send(out)
|
|
|
|
next_ = now + delay
|
|
|
|
delay *= 2
|
|
|
|
|
|
|
|
zc.wait(min(next_, last) - now)
|
|
|
|
now = time.time()
|
|
|
|
finally:
|
|
|
|
zc.remove_listener(self)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
class DashboardStatus(RecordUpdateListener, threading.Thread):
|
2021-09-05 22:22:15 +02:00
|
|
|
PING_AFTER = 15 * 1000 # Send new mDNS request after 15 seconds
|
|
|
|
OFFLINE_AFTER = PING_AFTER * 2 # Offline if no mDNS response after 30 seconds
|
|
|
|
|
|
|
|
def __init__(self, zc: Zeroconf, on_update) -> None:
|
2019-02-10 16:57:34 +01:00
|
|
|
threading.Thread.__init__(self)
|
|
|
|
self.zc = zc
|
2021-09-05 22:22:15 +02:00
|
|
|
self.query_hosts: set[str] = set()
|
2021-09-06 04:48:28 +02:00
|
|
|
self.key_to_host: Dict[str, str] = {}
|
2019-02-10 16:57:34 +01:00
|
|
|
self.stop_event = threading.Event()
|
|
|
|
self.query_event = threading.Event()
|
|
|
|
self.on_update = on_update
|
|
|
|
|
2021-09-05 22:22:15 +02:00
|
|
|
def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
|
|
|
|
pass
|
2019-02-10 16:57:34 +01:00
|
|
|
|
2021-09-06 04:48:28 +02:00
|
|
|
def request_query(self, hosts: Dict[str, str]) -> None:
|
2019-12-07 18:28:55 +01:00
|
|
|
self.query_hosts = set(hosts.values())
|
2019-02-10 16:57:34 +01:00
|
|
|
self.key_to_host = hosts
|
|
|
|
self.query_event.set()
|
|
|
|
|
2021-09-05 22:22:15 +02:00
|
|
|
def stop(self) -> None:
|
2019-02-10 16:57:34 +01:00
|
|
|
self.stop_event.set()
|
|
|
|
self.query_event.set()
|
|
|
|
|
2021-09-05 22:22:15 +02:00
|
|
|
def host_status(self, key: str) -> bool:
|
|
|
|
entries = self.zc.cache.entries_with_name(key)
|
|
|
|
if not entries:
|
|
|
|
return False
|
|
|
|
now = time.time() * 1000
|
2019-02-10 16:57:34 +01:00
|
|
|
|
2021-09-05 22:22:15 +02:00
|
|
|
return any(
|
|
|
|
(entry.created + DashboardStatus.OFFLINE_AFTER) >= now for entry in entries
|
|
|
|
)
|
|
|
|
|
|
|
|
def run(self) -> None:
|
|
|
|
self.zc.add_listener(self, None)
|
2019-02-10 16:57:34 +01:00
|
|
|
while not self.stop_event.is_set():
|
2021-09-05 22:22:15 +02:00
|
|
|
self.on_update(
|
|
|
|
{key: self.host_status(host) for key, host in self.key_to_host.items()}
|
|
|
|
)
|
|
|
|
now = time.time() * 1000
|
2019-02-10 16:57:34 +01:00
|
|
|
for host in self.query_hosts:
|
2021-09-05 22:22:15 +02:00
|
|
|
entries = self.zc.cache.entries_with_name(host)
|
|
|
|
if not entries or all(
|
|
|
|
(entry.created + DashboardStatus.PING_AFTER) <= now
|
|
|
|
for entry in entries
|
2021-03-07 20:03:16 +01:00
|
|
|
):
|
2019-02-10 16:57:34 +01:00
|
|
|
out = DNSOutgoing(_FLAGS_QR_QUERY)
|
2021-03-07 20:03:16 +01:00
|
|
|
out.add_question(DNSQuestion(host, _TYPE_A, _CLASS_IN))
|
2019-02-10 16:57:34 +01:00
|
|
|
self.zc.send(out)
|
|
|
|
self.query_event.wait()
|
|
|
|
self.query_event.clear()
|
|
|
|
self.zc.remove_listener(self)
|
|
|
|
|
|
|
|
|
2021-09-05 22:22:15 +02:00
|
|
|
class EsphomeZeroconf(Zeroconf):
|
|
|
|
def resolve_host(self, host: str, timeout=3.0):
|
2019-02-10 16:57:34 +01:00
|
|
|
info = HostResolver(host)
|
|
|
|
if info.request(self, timeout):
|
|
|
|
return socket.inet_ntoa(info.address)
|
|
|
|
return None
|