diff --git a/.coveragerc b/.coveragerc index 0c7f17f3098630816fb22aa37191e7538f5bd0c3..edf66c6252b3d55ad07b5dea3ceba3472f03c97b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -335,6 +335,7 @@ omit = homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/ebox.py + homeassistant/components/sensor/eddystone_temperature.py homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/emoncms.py homeassistant/components/sensor/fastdotcom.py diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py new file mode 100644 index 0000000000000000000000000000000000000000..de51ff0d373bd82451ac81df729249f8571a2e1c --- /dev/null +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -0,0 +1,189 @@ +"""Read temperature information from Eddystone beacons. + +Your beacons must be configured to transmit UID (for identification) and TLM +(for temperature) frames. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.eddystone_temperature/ + +Original version of this code (for Skybeacons) by anpetrov. +https://github.com/anpetrov/skybeacon +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START) + +REQUIREMENTS = ['beacontools[scan]==1.0.1'] + +_LOGGER = logging.getLogger(__name__) + +# constants +CONF_BEACONS = 'beacons' +CONF_BT_DEVICE_ID = 'bt_device_id' +CONF_INSTANCE = 'instance' +CONF_NAMESPACE = 'namespace' + +BEACON_SCHEMA = vol.Schema({ + vol.Required(CONF_NAMESPACE): cv.string, + vol.Required(CONF_INSTANCE): cv.string, + vol.Optional(CONF_NAME): cv.string +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_BT_DEVICE_ID, default=0): cv.positive_int, + vol.Required(CONF_BEACONS): vol.Schema({cv.string: BEACON_SCHEMA}), +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Validate configuration, create devices and start monitoring thread.""" + _LOGGER.debug("Setting up...") + + bt_device_id = config.get("bt_device_id") + + beacons = config.get("beacons") + devices = [] + + for dev_name, properties in beacons.items(): + namespace = get_from_conf(properties, "namespace", 20) + instance = get_from_conf(properties, "instance", 12) + name = properties.get(CONF_NAME, dev_name) + + if instance is None or namespace is None: + _LOGGER.error("Skipping %s", dev_name) + continue + else: + devices.append(EddystoneTemp(name, namespace, instance)) + + if len(devices) > 0: + mon = Monitor(hass, devices, bt_device_id) + + def monitor_stop(_service_or_event): + """Stop the monitor thread.""" + _LOGGER.info("Stopping scanner for eddystone beacons") + mon.stop() + + def monitor_start(_service_or_event): + """Start the monitor thread.""" + _LOGGER.info("Starting scanner for eddystone beacons") + mon.start() + + add_devices(devices) + mon.start() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, monitor_stop) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, monitor_start) + else: + _LOGGER.warning("No devices were added") + + +def get_from_conf(config, config_key, length): + """Retrieve value from config and validate length.""" + string = config.get(config_key) + if len(string) != length: + _LOGGER.error("Error in config parameter \"%s\": Must be exactly %d " + "bytes. Device will not be added.", + config_key, length/2) + return None + else: + return string + + +class EddystoneTemp(Entity): + """Representation of a temperature sensor.""" + + def __init__(self, name, namespace, instance): + """Initialize a sensor.""" + self._name = name + self.namespace = namespace + self.instance = instance + self.bt_addr = None + self.temperature = STATE_UNKNOWN + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self.temperature + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return TEMP_CELSIUS + + @property + def should_poll(self): + """Hass should not poll for state.""" + return False + + +class Monitor(object): + """Continously scan for BLE advertisements.""" + + def __init__(self, hass, devices, bt_device_id): + """Construct interface object.""" + self.hass = hass + + # list of beacons to monitor + self.devices = devices + # number of the bt device (hciX) + self.bt_device_id = bt_device_id + + def callback(bt_addr, _, packet, additional_info): + """Callback for new packets.""" + self.process_packet(additional_info['namespace'], + additional_info['instance'], + packet.temperature) + + # pylint: disable=import-error + from beacontools import (BeaconScanner, EddystoneFilter, + EddystoneTLMFrame) + # Create a device filter for each device + device_filters = [EddystoneFilter(d.namespace, d.instance) + for d in devices] + + self.scanner = BeaconScanner(callback, bt_device_id, device_filters, + EddystoneTLMFrame) + self.scanning = False + + def start(self): + """Continously scan for BLE advertisements.""" + if not self.scanning: + self.scanner.start() + self.scanning = True + else: + _LOGGER.debug("Warning: start() called, but scanner is already" + " running") + + def process_packet(self, namespace, instance, temperature): + """Assign temperature to hass device.""" + _LOGGER.debug("Received temperature for <%s,%s>: %d", + namespace, instance, temperature) + + for dev in self.devices: + if dev.namespace == namespace and dev.instance == instance: + if dev.temperature != temperature: + dev.temperature = temperature + dev.schedule_update_ha_state() + + def stop(self): + """Signal runner to stop and join thread.""" + if self.scanning: + _LOGGER.debug("Stopping...") + self.scanner.stop() + _LOGGER.debug("Stopped") + self.scanning = False + else: + _LOGGER.debug("Warning: stop() called but scanner was not" + " running.") diff --git a/requirements_all.txt b/requirements_all.txt index 8cb9cf2fa3b806b247d21f170c8e14d199022c45..b656daf4a115ed25bdd38756d8212bdddb2c5951 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -69,6 +69,9 @@ astral==1.4 # homeassistant.components.sensor.linux_battery batinfo==0.4.2 +# homeassistant.components.sensor.eddystone_temperature +# beacontools[scan]==1.0.1 + # homeassistant.components.device_tracker.linksys_ap # homeassistant.components.sensor.scrape beautifulsoup4==4.5.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 16493dc884ecb5ca9e188cfc112a76d0aa789417..94635a585251b4df999603fe21d5a4008262f23c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -13,6 +13,7 @@ COMMENT_REQUIREMENTS = ( 'Adafruit_BBIO', 'fritzconnection', 'pybluez', + 'beacontools', 'bluepy', 'python-lirc', 'gattlib',