* Helpful utils that will allow to reduce common code * Update config with new options for blescanmulti * Default values for blescanmulti if not provided by config file * Allow to configure scan behavior * Allow to configure returned payload * Initial version of saving last state * Support class that will remember last status of device * Add docs for available options * Move scanner to instance, this will prevent recreating it each time it is required to scan * Use items instead of values. We require here to have pair of key and value, not just value * Make sure that mac address is in lower case * Optimize searching for devices, use hash maps * Remove unused method * Because YAML is already doing some conversions this change will allow to always get proper value * Fix issue. Mac address and name was provided in wrong order * Use worker to format topic * Send also RSSI info
104 lines
3.2 KiB
Python
104 lines
3.2 KiB
Python
import time
|
|
from interruptingcow import timeout
|
|
from bluepy.btle import Scanner, DefaultDelegate
|
|
from mqtt import MqttMessage
|
|
from utils import booleanize
|
|
from workers.base import BaseWorker
|
|
from logger import _LOGGER
|
|
|
|
REQUIREMENTS = ['bluepy']
|
|
|
|
|
|
class ScanDelegate(DefaultDelegate):
|
|
def __init__(self):
|
|
DefaultDelegate.__init__(self)
|
|
|
|
def handleDiscovery(self, dev, isNewDev, isNewData):
|
|
if isNewDev:
|
|
_LOGGER.debug("Discovered new device: %s" % dev.addr)
|
|
|
|
|
|
class BleDeviceStatus:
|
|
def __init__(self, worker, mac: str, name: str, available: bool = False, last_status_time: float = None,
|
|
message_sent: bool = True):
|
|
if last_status_time is None:
|
|
last_status_time = time.time()
|
|
|
|
self.worker = worker # type: BlescanmultiWorker
|
|
self.mac = mac.lower()
|
|
self.name = name
|
|
self.available = available
|
|
self.last_status_time = last_status_time
|
|
self.message_sent = message_sent
|
|
|
|
def set_status(self, available):
|
|
if available != self.available:
|
|
self.available = available
|
|
self.last_status_time = time.time()
|
|
self.message_sent = False
|
|
|
|
def _timeout(self):
|
|
if self.available:
|
|
return self.worker.available_timeout
|
|
else:
|
|
return self.worker.unavailable_timeout
|
|
|
|
def has_time_elapsed(self):
|
|
elapsed = time.time() - self.last_status_time
|
|
return elapsed > self._timeout()
|
|
|
|
def payload(self):
|
|
if self.available:
|
|
return self.worker.available_payload
|
|
else:
|
|
return self.worker.unavailable_payload
|
|
|
|
def generate_messages(self, device):
|
|
messages = []
|
|
if not self.message_sent and self.has_time_elapsed():
|
|
self.message_sent = True
|
|
messages.append(
|
|
MqttMessage(topic=self.worker.format_topic('presence/{}'.format(self.name)), payload=self.payload())
|
|
)
|
|
if self.available:
|
|
messages.append(
|
|
MqttMessage(topic=self.worker.format_topic('presence/{}/rssi'.format(self.name)), payload=device.rssi)
|
|
)
|
|
return messages
|
|
|
|
|
|
class BlescanmultiWorker(BaseWorker):
|
|
# Default values
|
|
devices = {}
|
|
# Payload that should be send when device is available
|
|
available_payload = 'home' # type: str
|
|
# Payload that should be send when device is unavailable
|
|
unavailable_payload = 'not_home' # type: str
|
|
# After what time (in seconds) we should inform that device is available (default: 0 seconds)
|
|
available_timeout = 0 # type: float
|
|
# After what time (in seconds) we should inform that device is unavailable (default: 60 seconds)
|
|
unavailable_timeout = 60 # type: float
|
|
scan_timeout = 10. # type: float
|
|
scan_passive = True # type: str or bool
|
|
|
|
def __init__(self, **kwargs):
|
|
super(BlescanmultiWorker, self).__init__(**kwargs)
|
|
self.scanner = Scanner().withDelegate(ScanDelegate())
|
|
self.last_status = [
|
|
BleDeviceStatus(self, mac, name) for name, mac in self.devices.items()
|
|
]
|
|
|
|
def status_update(self):
|
|
devices = self.scanner.scan(float(self.scan_timeout), passive=booleanize(self.scan_passive))
|
|
mac_addresses = {
|
|
device.addr: device for device in devices
|
|
}
|
|
ret = []
|
|
|
|
for status in self.last_status:
|
|
device = mac_addresses.get(status.mac, None)
|
|
status.set_status(device is not None)
|
|
ret += status.generate_messages(device)
|
|
|
|
return ret
|