Skip to content
Snippets Groups Projects
Commit 752a4b95 authored by John Mihalic's avatar John Mihalic Committed by Paulus Schoutsen
Browse files

Add Eight sleep component (#7275)

* Eight Sleep Implementation

* Update coverage

* Update hass requirements

* Remove unnecessary debug statements

* Bump version to fix date error

* Address comments

* Update requirements
parent 350a6fd5
No related branches found
No related tags found
No related merge requests found
......@@ -158,6 +158,9 @@ omit =
homeassistant/components/zha/const.py
homeassistant/components/*/zha.py
homeassistant/components/eight_sleep.py
homeassistant/components/*/eight_sleep.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/nx584.py
......
"""
Support for Eight Sleep binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.eight_sleep/
"""
import logging
import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.eight_sleep import (
DATA_EIGHT, EightSleepHeatEntity, CONF_BINARY_SENSORS, NAME_MAP)
DEPENDENCIES = ['eight_sleep']
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the eight sleep binary sensor."""
if discovery_info is None:
return
name = 'Eight'
sensors = discovery_info[CONF_BINARY_SENSORS]
eight = hass.data[DATA_EIGHT]
all_sensors = []
for sensor in sensors:
all_sensors.append(EightHeatSensor(name, eight, sensor))
async_add_devices(all_sensors, True)
class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice):
"""Representation of a eight sleep heat-based sensor."""
def __init__(self, name, eight, sensor):
"""Initialize the sensor."""
super().__init__(eight)
self._sensor = sensor
self._mapped_name = NAME_MAP.get(self._sensor, self._sensor)
self._name = '{} {}'.format(name, self._mapped_name)
self._state = None
self._side = self._sensor.split('_')[0]
self._userid = self._eight.fetch_userid(self._side)
self._usrobj = self._eight.users[self._userid]
_LOGGER.debug('Presence Sensor: %s, Side: %s, User: %s',
self._sensor, self._side, self._userid)
@property
def name(self):
"""Return the name of the sensor, if any."""
return self._name
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
self._state = self._usrobj.bed_presence
"""
Support for Eight smart mattress covers and mattresses.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/eight_sleep/
"""
import asyncio
import logging
import os
from datetime import timedelta
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_BINARY_SENSORS,
ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_send, async_dispatcher_connect)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
REQUIREMENTS = ['pyeight==0.0.4']
_LOGGER = logging.getLogger(__name__)
CONF_PARTNER = 'partner'
DATA_EIGHT = 'eight_sleep'
DEFAULT_PARTNER = False
DOMAIN = 'eight_sleep'
HEAT_ENTITY = 'heat'
USER_ENTITY = 'user'
HEAT_SCAN_INTERVAL = timedelta(seconds=60)
USER_SCAN_INTERVAL = timedelta(seconds=300)
SIGNAL_UPDATE_HEAT = 'eight_heat_update'
SIGNAL_UPDATE_USER = 'eight_user_update'
NAME_MAP = {
'left_current_sleep': 'Left Sleep Session',
'left_last_sleep': 'Left Previous Sleep Session',
'left_bed_state': 'Left Bed State',
'left_presence': 'Left Bed Presence',
'left_bed_temp': 'Left Bed Temperature',
'left_sleep_stage': 'Left Sleep Stage',
'right_current_sleep': 'Right Sleep Session',
'right_last_sleep': 'Right Previous Sleep Session',
'right_bed_state': 'Right Bed State',
'right_presence': 'Right Bed Presence',
'right_bed_temp': 'Right Bed Temperature',
'right_sleep_stage': 'Right Sleep Stage',
'room_temp': 'Room Temperature',
}
SENSORS = ['current_sleep',
'last_sleep',
'bed_state',
'bed_temp',
'sleep_stage']
SERVICE_HEAT_SET = 'heat_set'
ATTR_TARGET_HEAT = 'target'
ATTR_HEAT_DURATION = 'duration'
VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100))
VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800))
SERVICE_EIGHT_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_TARGET_HEAT: VALID_TARGET_HEAT,
ATTR_HEAT_DURATION: VALID_DURATION,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PARTNER, default=DEFAULT_PARTNER): cv.boolean,
}),
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the Eight Sleep component."""
from pyeight.eight import EightSleep
conf = config.get(DOMAIN)
user = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
partner = conf.get(CONF_PARTNER)
if hass.config.time_zone is None:
_LOGGER.error('Timezone is not set in Home Assistant.')
return False
timezone = hass.config.time_zone
eight = EightSleep(user, password, timezone, partner, None, hass.loop)
hass.data[DATA_EIGHT] = eight
# Authenticate, build sensors
success = yield from eight.start()
if not success:
# Authentication failed, cannot continue
return False
@asyncio.coroutine
def async_update_heat_data(now):
"""Update heat data from eight in HEAT_SCAN_INTERVAL."""
yield from eight.update_device_data()
async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT)
async_track_point_in_utc_time(
hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL)
@asyncio.coroutine
def async_update_user_data(now):
"""Update user data from eight in USER_SCAN_INTERVAL."""
yield from eight.update_user_data()
async_dispatcher_send(hass, SIGNAL_UPDATE_USER)
async_track_point_in_utc_time(
hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL)
yield from async_update_heat_data(None)
yield from async_update_user_data(None)
# Load sub components
sensors = []
binary_sensors = []
if eight.users:
for user in eight.users:
obj = eight.users[user]
for sensor in SENSORS:
sensors.append('{}_{}'.format(obj.side, sensor))
binary_sensors.append('{}_presence'.format(obj.side))
sensors.append('room_temp')
hass.async_add_job(discovery.async_load_platform(
hass, 'sensor', DOMAIN, {
CONF_SENSORS: sensors,
}, config))
hass.async_add_job(discovery.async_load_platform(
hass, 'binary_sensor', DOMAIN, {
CONF_BINARY_SENSORS: binary_sensors,
}, config))
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
def async_service_handler(service):
"""Handle eight sleep service calls."""
params = service.data.copy()
sensor = params.pop(ATTR_ENTITY_ID, None)
target = params.pop(ATTR_TARGET_HEAT, None)
duration = params.pop(ATTR_HEAT_DURATION, 0)
for sens in sensor:
side = sens.split('_')[1]
userid = eight.fetch_userid(side)
usrobj = eight.users[userid]
yield from usrobj.set_heating_level(target, duration)
async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT)
# Register services
hass.services.async_register(
DOMAIN, SERVICE_HEAT_SET, async_service_handler,
descriptions[DOMAIN].get(SERVICE_HEAT_SET),
schema=SERVICE_EIGHT_SCHEMA)
@asyncio.coroutine
def stop_eight(event):
"""Handle stopping eight api session."""
yield from eight.stop()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight)
return True
class EightSleepUserEntity(Entity):
"""The Eight Sleep device entity."""
def __init__(self, eight):
"""Initialize the data oject."""
self._eight = eight
@asyncio.coroutine
def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_eight_user_update():
"""Update callback."""
self.hass.async_add_job(self.async_update_ha_state(True))
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_USER, async_eight_user_update)
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return False
class EightSleepHeatEntity(Entity):
"""The Eight Sleep device entity."""
def __init__(self, eight):
"""Initialize the data oject."""
self._eight = eight
@asyncio.coroutine
def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_eight_heat_update():
"""Update callback."""
self.hass.async_add_job(self.async_update_ha_state(True))
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update)
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return False
"""
Support for Eight Sleep sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.eight_sleep/
"""
import logging
import asyncio
from homeassistant.components.eight_sleep import (
DATA_EIGHT, EightSleepHeatEntity, EightSleepUserEntity,
CONF_SENSORS, NAME_MAP)
DEPENDENCIES = ['eight_sleep']
ATTR_ROOM_TEMP = 'Room Temperature'
ATTR_AVG_ROOM_TEMP = 'Average Room Temperature'
ATTR_BED_TEMP = 'Bed Temperature'
ATTR_AVG_BED_TEMP = 'Average Bed Temperature'
ATTR_RESP_RATE = 'Respiratory Rate'
ATTR_AVG_RESP_RATE = 'Average Respiratory Rate'
ATTR_HEART_RATE = 'Heart Rate'
ATTR_AVG_HEART_RATE = 'Average Heart Rate'
ATTR_SLEEP_DUR = 'Time Slept'
ATTR_LIGHT_PERC = 'Light Sleep %'
ATTR_DEEP_PERC = 'Deep Sleep %'
ATTR_TNT = 'Tosses & Turns'
ATTR_SLEEP_STAGE = 'Sleep Stage'
ATTR_TARGET_HEAT = 'Target Heating Level'
ATTR_ACTIVE_HEAT = 'Heating Active'
ATTR_DURATION_HEAT = 'Heating Time Remaining'
ATTR_LAST_SEEN = 'Last In Bed'
ATTR_PROCESSING = 'Processing'
ATTR_SESSION_START = 'Session Start'
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the eight sleep sensors."""
if discovery_info is None:
return
name = 'Eight'
sensors = discovery_info[CONF_SENSORS]
eight = hass.data[DATA_EIGHT]
if hass.config.units.is_metric:
units = 'si'
else:
units = 'us'
all_sensors = []
for sensor in sensors:
if 'bed_state' in sensor:
all_sensors.append(EightHeatSensor(name, eight, sensor))
elif 'room_temp' in sensor:
all_sensors.append(EightRoomSensor(name, eight, sensor, units))
else:
all_sensors.append(EightUserSensor(name, eight, sensor, units))
async_add_devices(all_sensors, True)
class EightHeatSensor(EightSleepHeatEntity):
"""Representation of a eight sleep heat-based sensor."""
def __init__(self, name, eight, sensor):
"""Initialize the sensor."""
super().__init__(eight)
self._sensor = sensor
self._mapped_name = NAME_MAP.get(self._sensor, self._sensor)
self._name = '{} {}'.format(name, self._mapped_name)
self._state = None
self._side = self._sensor.split('_')[0]
self._userid = self._eight.fetch_userid(self._side)
self._usrobj = self._eight.users[self._userid]
_LOGGER.debug('Heat Sensor: %s, Side: %s, User: %s',
self._sensor, self._side, self._userid)
@property
def name(self):
"""Return the name of the sensor, if any."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return '%'
@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug('Updating Heat sensor: %s', self._sensor)
self._state = self._usrobj.heating_level
@property
def device_state_attributes(self):
"""Return device state attributes."""
state_attr = {ATTR_TARGET_HEAT: self._usrobj.target_heating_level}
state_attr[ATTR_ACTIVE_HEAT] = self._usrobj.now_heating
state_attr[ATTR_DURATION_HEAT] = self._usrobj.heating_remaining
state_attr[ATTR_LAST_SEEN] = self._usrobj.last_seen
return state_attr
class EightUserSensor(EightSleepUserEntity):
"""Representation of a eight sleep user-based sensor."""
def __init__(self, name, eight, sensor, units):
"""Initialize the sensor."""
super().__init__(eight)
self._sensor = sensor
self._sensor_root = self._sensor.split('_', 1)[1]
self._mapped_name = NAME_MAP.get(self._sensor, self._sensor)
self._name = '{} {}'.format(name, self._mapped_name)
self._state = None
self._attr = None
self._units = units
self._side = self._sensor.split('_', 1)[0]
self._userid = self._eight.fetch_userid(self._side)
self._usrobj = self._eight.users[self._userid]
_LOGGER.debug('User Sensor: %s, Side: %s, User: %s',
self._sensor, self._side, self._userid)
@property
def name(self):
"""Return the name of the sensor, if any."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
if 'current_sleep' in self._sensor or 'last_sleep' in self._sensor:
return 'Score'
elif 'bed_temp' in self._sensor:
if self._units == 'si':
return '°C'
else:
return '°F'
@property
def icon(self):
"""Icon to use in the frontend, if any."""
if 'bed_temp' in self._sensor:
return 'mdi:thermometer'
@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug('Updating User sensor: %s', self._sensor)
if 'current' in self._sensor:
self._state = self._usrobj.current_sleep_score
self._attr = self._usrobj.current_values
elif 'last' in self._sensor:
self._state = self._usrobj.last_sleep_score
self._attr = self._usrobj.last_values
elif 'bed_temp' in self._sensor:
temp = self._usrobj.current_values['bed_temp']
if self._units == 'si':
self._state = round(temp, 2)
else:
self._state = round((temp*1.8)+32, 2)
elif 'sleep_stage' in self._sensor:
self._state = self._usrobj.current_values['stage']
@property
def device_state_attributes(self):
"""Return device state attributes."""
if self._attr is None:
# Skip attributes if sensor type doesn't support
return None
state_attr = {ATTR_SESSION_START: self._attr['date']}
state_attr[ATTR_TNT] = self._attr['tnt']
state_attr[ATTR_PROCESSING] = self._attr['processing']
sleep_time = sum(self._attr['breakdown'].values()) - \
self._attr['breakdown']['awake']
state_attr[ATTR_SLEEP_DUR] = sleep_time
state_attr[ATTR_LIGHT_PERC] = round((
self._attr['breakdown']['light'] / sleep_time) * 100, 2)
state_attr[ATTR_DEEP_PERC] = round((
self._attr['breakdown']['deep'] / sleep_time) * 100, 2)
if self._units == 'si':
room_temp = round(self._attr['room_temp'], 2)
bed_temp = round(self._attr['bed_temp'], 2)
else:
room_temp = round((self._attr['room_temp']*1.8)+32, 2)
bed_temp = round((self._attr['bed_temp']*1.8)+32, 2)
if 'current' in self._sensor_root:
state_attr[ATTR_RESP_RATE] = round(self._attr['resp_rate'], 2)
state_attr[ATTR_HEART_RATE] = round(self._attr['heart_rate'], 2)
state_attr[ATTR_SLEEP_STAGE] = self._attr['stage']
state_attr[ATTR_ROOM_TEMP] = room_temp
state_attr[ATTR_BED_TEMP] = bed_temp
elif 'last' in self._sensor_root:
state_attr[ATTR_AVG_RESP_RATE] = round(self._attr['resp_rate'], 2)
state_attr[ATTR_AVG_HEART_RATE] = round(
self._attr['heart_rate'], 2)
state_attr[ATTR_AVG_ROOM_TEMP] = room_temp
state_attr[ATTR_AVG_BED_TEMP] = bed_temp
return state_attr
class EightRoomSensor(EightSleepUserEntity):
"""Representation of a eight sleep room sensor."""
def __init__(self, name, eight, sensor, units):
"""Initialize the sensor."""
super().__init__(eight)
self._sensor = sensor
self._mapped_name = NAME_MAP.get(self._sensor, self._sensor)
self._name = '{} {}'.format(name, self._mapped_name)
self._state = None
self._attr = None
self._units = units
@property
def name(self):
"""Return the name of the sensor, if any."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug('Updating Room sensor: %s', self._sensor)
temp = self._eight.room_temperature()
if self._units == 'si':
self._state = round(temp, 2)
else:
self._state = round((temp*1.8)+32, 2)
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
if self._units == 'si':
return '°C'
else:
return '°F'
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return 'mdi:thermometer'
......@@ -388,3 +388,17 @@ hassio:
addon:
description: Name of addon.
example: 'smb_config'
eight_sleep:
heat_set:
description: Set heating level for eight sleep.
fields:
entity_id:
description: Entity id of the bed state to adjust.
example: 'sensor.eight_left_bed_state'
target:
description: Target heating level from 0-100.
example: 35
duration:
description: Duration to heat at the target level in seconds.
example: 3600
......@@ -530,6 +530,9 @@ pydroid-ipcam==0.8
# homeassistant.components.sensor.ebox
pyebox==0.1.0
# homeassistant.components.eight_sleep
pyeight==0.0.4
# homeassistant.components.notify.html5
pyelliptic==1.5.7
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment