From 4c23ccad983e44d3cac7104c4f11434d744dda24 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Sat, 16 Feb 2019 03:12:16 -0600 Subject: [PATCH] Owlet baby monitor component (#21108) --- .coveragerc | 1 + homeassistant/components/owlet/__init__.py | 70 ++++++++++++ .../components/owlet/binary_sensor.py | 82 ++++++++++++++ homeassistant/components/owlet/const.py | 6 + homeassistant/components/owlet/sensor.py | 103 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 265 insertions(+) create mode 100644 homeassistant/components/owlet/__init__.py create mode 100644 homeassistant/components/owlet/binary_sensor.py create mode 100644 homeassistant/components/owlet/const.py create mode 100644 homeassistant/components/owlet/sensor.py diff --git a/.coveragerc b/.coveragerc index dec0594e7a3..8b51a34df61 100644 --- a/.coveragerc +++ b/.coveragerc @@ -376,6 +376,7 @@ omit = homeassistant/components/openuv/__init__.py homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/sensor.py + homeassistant/components/owlet/* homeassistant/components/pilight/* homeassistant/components/plum_lightpad/* homeassistant/components/point/* diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py new file mode 100644 index 00000000000..c29f937183f --- /dev/null +++ b/homeassistant/components/owlet/__init__.py @@ -0,0 +1,70 @@ +"""Support for Owlet baby monitors.""" +import logging + +import voluptuous as vol + +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_NAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +from .const import SENSOR_MOVEMENT, SENSOR_BASE_STATION, SENSOR_HEART_RATE, \ + SENSOR_OXYGEN_LEVEL + +REQUIREMENTS = ['pyowlet==1.0.2'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'owlet' + +SENSOR_TYPES = [ + SENSOR_OXYGEN_LEVEL, + SENSOR_HEART_RATE, + SENSOR_BASE_STATION, + SENSOR_MOVEMENT +] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up owlet component.""" + from pyowlet.PyOwlet import PyOwlet + + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + name = config[DOMAIN].get(CONF_NAME) + + try: + device = PyOwlet(username, password) + except KeyError: + _LOGGER.error('Owlet authentication failed. Please verify your ' + 'credentials are correct.') + return False + + device.update_properties() + + if not name: + name = '{}\'s Owlet'.format(device.baby_name) + + hass.data[DOMAIN] = OwletDevice(device, name, SENSOR_TYPES) + + load_platform(hass, 'sensor', DOMAIN, {}, config) + load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + return True + + +class OwletDevice(): + """Represents a configured Owlet device.""" + + def __init__(self, device, name, monitor): + """Initialize device.""" + self.name = name + self.monitor = monitor + self.device = device diff --git a/homeassistant/components/owlet/binary_sensor.py b/homeassistant/components/owlet/binary_sensor.py new file mode 100644 index 00000000000..cb66278150a --- /dev/null +++ b/homeassistant/components/owlet/binary_sensor.py @@ -0,0 +1,82 @@ +"""Support for Owlet binary sensors.""" +from datetime import timedelta + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.owlet import DOMAIN as OWLET_DOMAIN +from homeassistant.util import dt as dt_util + +from .const import SENSOR_BASE_STATION, SENSOR_MOVEMENT + +SCAN_INTERVAL = timedelta(seconds=120) + +BINARY_CONDITIONS = { + SENSOR_BASE_STATION: { + 'name': 'Base Station', + 'device_class': 'power' + }, + SENSOR_MOVEMENT: { + 'name': 'Movement', + 'device_class': 'motion' + } +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up owlet binary sensor.""" + if discovery_info is None: + return + + device = hass.data[OWLET_DOMAIN] + + entities = [] + for condition in BINARY_CONDITIONS: + if condition in device.monitor: + entities.append(OwletBinarySensor(device, condition)) + + add_entities(entities, True) + + +class OwletBinarySensor(BinarySensorDevice): + """Representation of owlet binary sensor.""" + + def __init__(self, device, condition): + """Init owlet binary sensor.""" + self._device = device + self._condition = condition + self._state = None + self._base_on = False + self._prop_expiration = None + self._is_charging = None + + @property + def name(self): + """Return sensor name.""" + return '{} {}'.format(self._device.name, + BINARY_CONDITIONS[self._condition]['name']) + + @property + def is_on(self): + """Return current state of sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return BINARY_CONDITIONS[self._condition]['device_class'] + + def update(self): + """Update state of sensor.""" + self._base_on = self._device.device.base_station_on + self._prop_expiration = self._device.device.prop_expire_time + self._is_charging = self._device.device.charge_status > 0 + + # handle expired values + if self._prop_expiration < dt_util.now().timestamp(): + self._state = False + return + + if self._condition == 'movement': + if not self._base_on or self._is_charging: + return False + + self._state = getattr(self._device.device, self._condition) diff --git a/homeassistant/components/owlet/const.py b/homeassistant/components/owlet/const.py new file mode 100644 index 00000000000..f8d4db3ec1e --- /dev/null +++ b/homeassistant/components/owlet/const.py @@ -0,0 +1,6 @@ +"""Constants for Owlet component.""" +SENSOR_OXYGEN_LEVEL = 'oxygen_level' +SENSOR_HEART_RATE = 'heart_rate' + +SENSOR_BASE_STATION = 'base_station_on' +SENSOR_MOVEMENT = 'movement' diff --git a/homeassistant/components/owlet/sensor.py b/homeassistant/components/owlet/sensor.py new file mode 100644 index 00000000000..b91cc387718 --- /dev/null +++ b/homeassistant/components/owlet/sensor.py @@ -0,0 +1,103 @@ +"""Support for Owlet sensors.""" +from datetime import timedelta + +from homeassistant.components.owlet import DOMAIN as OWLET_DOMAIN +from homeassistant.helpers.entity import Entity +from homeassistant.util import dt as dt_util + +from .const import SENSOR_HEART_RATE, SENSOR_OXYGEN_LEVEL + +SCAN_INTERVAL = timedelta(seconds=120) + +SENSOR_CONDITIONS = { + SENSOR_OXYGEN_LEVEL: { + 'name': 'Oxygen Level', + 'device_class': None + }, + SENSOR_HEART_RATE: { + 'name': 'Heart Rate', + 'device_class': None + } +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up owlet binary sensor.""" + if discovery_info is None: + return + + device = hass.data[OWLET_DOMAIN] + + entities = [] + for condition in SENSOR_CONDITIONS: + if condition in device.monitor: + entities.append(OwletSensor(device, condition)) + + add_entities(entities, True) + + +class OwletSensor(Entity): + """Representation of Owlet sensor.""" + + def __init__(self, device, condition): + """Init owlet binary sensor.""" + self._device = device + self._condition = condition + self._state = None + self._prop_expiration = None + self.is_charging = None + self.battery_level = None + self.sock_off = None + self.sock_connection = None + self._movement = None + + @property + def name(self): + """Return sensor name.""" + return '{} {}'.format(self._device.name, + SENSOR_CONDITIONS[self._condition]['name']) + + @property + def state(self): + """Return current state of sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return SENSOR_CONDITIONS[self._condition]['device_class'] + + @property + def device_state_attributes(self): + """Return state attributes.""" + attributes = { + 'battery_charging': self.is_charging, + 'battery_level': self.battery_level, + 'sock_off': self.sock_off, + 'sock_connection': self.sock_connection + } + + return attributes + + def update(self): + """Update state of sensor.""" + self.is_charging = self._device.device.charge_status + self.battery_level = self._device.device.batt_level + self.sock_off = self._device.device.sock_off + self.sock_connection = self._device.device.sock_connection + self._movement = self._device.device.movement + self._prop_expiration = self._device.device.prop_expire_time + + value = getattr(self._device.device, self._condition) + + if self._condition == 'batt_level': + self._state = min(100, value) + return + + if not self._device.device.base_station_on \ + or self._device.device.charge_status > 0 \ + or self._prop_expiration < dt_util.now().timestamp() \ + or self._movement: + value = None + + self._state = value diff --git a/requirements_all.txt b/requirements_all.txt index b7f4a207296..7773f3bbbd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1188,6 +1188,9 @@ pyotgw==0.4b1 # homeassistant.components.sensor.otp pyotp==2.2.6 +# homeassistant.components.owlet +pyowlet==1.0.2 + # homeassistant.components.sensor.openweathermap # homeassistant.components.weather.openweathermap pyowm==2.10.0