diff --git a/config.yaml.example b/config.yaml.example index 50b2585..e84b53c 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -54,6 +54,12 @@ manager: beacon: 00:11:22:33:44:55 smartwath: 00:11:22:33:44:55 topic_prefix: blescan + available_payload: home + unavailable_payload: not_home + available_timeout: 0 + unavailable_timeout: 60 + scan_timeout: 10 + scan_passive: true update_interval: 60 toothbrush: args: diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..0696b41 --- /dev/null +++ b/utils.py @@ -0,0 +1,14 @@ + +true_statement = ('y', 'yes', 'on', '1', 'true', 't', ) + + +def booleanize(value) -> bool: + """ + This function will try to assume that provided string value is boolean in some way. It will accept a wide range + of values for strings like ('y', 'yes', 'on', '1', 'true' and 't'. Any other value will be treated as false + :param value: any value + :return: boolean statement + """ + if isinstance(value, str): + return value.lower() in true_statement + return bool(value) diff --git a/workers/blescanmulti.py b/workers/blescanmulti.py index 37bf761..612e9df 100644 --- a/workers/blescanmulti.py +++ b/workers/blescanmulti.py @@ -2,11 +2,13 @@ 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) @@ -15,25 +17,87 @@ 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): - def searchmac(self, devices, mac): - for dev in devices: - if dev.addr == mac.lower(): - return dev + # 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 - return None + 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): - scanner = Scanner().withDelegate(ScanDelegate()) - devices = scanner.scan(10.0) + devices = self.scanner.scan(float(self.scan_timeout), passive=booleanize(self.scan_passive)) + mac_addresses = { + device.addr: device for device in devices + } ret = [] - for name, mac in self.devices.items(): - device = self.searchmac(devices, mac) - if device is None: - ret.append(MqttMessage(topic=self.format_topic('presence/'+name), payload="0")) - else: - ret.append(MqttMessage(topic=self.format_topic('presence/'+name+'/rssi'), payload=device.rssi)) - ret.append(MqttMessage(topic=self.format_topic('presence/'+name), payload="1")) + 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