Skip to content
Merged
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,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
Expand Down
159 changes: 159 additions & 0 deletions homeassistant/components/sensor/eddystone_temperature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""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,
CONF_NAMESPACE, CONF_INSTANCE, CONF_BT_DEVICE_ID, CONF_BEACONS)

REQUIREMENTS = ['beacontools[scan]==1.0.1']

_LOGGER = logging.getLogger(__name__)

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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

"""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():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too many blank lines (2)

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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 1 blank line before a nested definition, found 0

"""Stop the monitor thread."""
_LOGGER.info("Stopping scanner for eddystone beacons")
mon.stop()

add_devices(devices)
mon.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, monitor_stop)
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


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)

def start(self):
"""Continously scan for BLE advertisements."""
self.scanner.start()

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:
dev.temperature = temperature

def stop(self):
"""Signal runner to stop and join thread."""
_LOGGER.debug("Stopping...")
self.scanner.stop()
_LOGGER.debug("Stopped")
4 changes: 4 additions & 0 deletions homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@
CONF_API_KEY = 'api_key'
CONF_AUTHENTICATION = 'authentication'
CONF_BASE = 'base'
CONF_BEACONS = 'beacons'
CONF_BEFORE = 'before'
CONF_BELOW = 'below'
CONF_BINARY_SENSORS = 'binary_sensors'
CONF_BLACKLIST = 'blacklist'
CONF_BRIGHTNESS = 'brightness'
CONF_BT_DEVICE_ID = 'bt_device_id'
CONF_CODE = 'code'
CONF_COLOR_TEMP = 'color_temp'
CONF_COMMAND = 'command'
Expand Down Expand Up @@ -104,6 +106,7 @@
CONF_HOSTS = 'hosts'
CONF_ICON = 'icon'
CONF_INCLUDE = 'include'
CONF_INSTANCE = 'instance'
CONF_ID = 'id'
CONF_LATITUDE = 'latitude'
CONF_LONGITUDE = 'longitude'
Expand All @@ -114,6 +117,7 @@
CONF_MONITORED_CONDITIONS = 'monitored_conditions'
CONF_MONITORED_VARIABLES = 'monitored_variables'
CONF_NAME = 'name'
CONF_NAMESPACE = 'namespace'
CONF_OFFSET = 'offset'
CONF_OPTIMISTIC = 'optimistic'
CONF_PACKAGES = 'packages'
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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
Expand Down
1 change: 1 addition & 0 deletions script/gen_requirements_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
'Adafruit_BBIO',
'fritzconnection',
'pybluez',
'beacontools',
'bluepy',
'python-lirc',
'gattlib',
Expand Down