SleepIQ component with sensor and binary sensor platforms (#3390)

Original from #2949
This commit is contained in:
Pascal Vizeli 2016-09-14 00:11:50 +02:00 committed by GitHub
parent de2eed3c9f
commit 1697a8c774
12 changed files with 540 additions and 0 deletions

View file

@ -0,0 +1,59 @@
"""
Support for SleepIQ sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.sleepiq/
"""
from homeassistant.components import sleepiq
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['sleepiq']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the SleepIQ sensors."""
if discovery_info is None:
return
data = sleepiq.DATA
data.update()
dev = list()
for bed_id, _ in data.beds.items():
for side in sleepiq.SIDES:
dev.append(IsInBedBinarySensor(
data,
bed_id,
side))
add_devices(dev)
# pylint: disable=too-many-instance-attributes
class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
"""Implementation of a SleepIQ presence sensor."""
def __init__(self, sleepiq_data, bed_id, side):
"""Initialize the sensor."""
sleepiq.SleepIQSensor.__init__(self,
sleepiq_data,
bed_id,
side)
self.type = sleepiq.IS_IN_BED
self._state = None
self._name = sleepiq.SENSOR_TYPES[self.type]
self.update()
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state is True
@property
def sensor_class(self):
"""Return the class of this sensor."""
return "occupancy"
def update(self):
"""Get the latest data from SleepIQ and updates the states."""
sleepiq.SleepIQSensor.update(self)
self._state = self.side.is_in_bed

View file

@ -0,0 +1,58 @@
"""
Support for SleepIQ sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.sleepiq/
"""
from homeassistant.components import sleepiq
DEPENDENCIES = ['sleepiq']
ICON = 'mdi:hotel'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the SleepIQ sensors."""
if discovery_info is None:
return
data = sleepiq.DATA
data.update()
dev = list()
for bed_id, _ in data.beds.items():
for side in sleepiq.SIDES:
dev.append(SleepNumberSensor(data, bed_id, side))
add_devices(dev)
# pylint: disable=too-few-public-methods, too-many-instance-attributes
class SleepNumberSensor(sleepiq.SleepIQSensor):
"""Implementation of a SleepIQ sensor."""
def __init__(self, sleepiq_data, bed_id, side):
"""Initialize the sensor."""
sleepiq.SleepIQSensor.__init__(self,
sleepiq_data,
bed_id,
side)
self._state = None
self.type = sleepiq.SLEEP_NUMBER
self._name = sleepiq.SENSOR_TYPES[self.type]
self.update()
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
def update(self):
"""Get the latest data from SleepIQ and updates the states."""
sleepiq.SleepIQSensor.update(self)
self._state = self.side.sleep_number

View file

@ -0,0 +1,130 @@
"""
Support for SleepIQ from SleepNumber.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sleepiq/
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.util import Throttle
from requests.exceptions import HTTPError
DOMAIN = 'sleepiq'
REQUIREMENTS = ['sleepyq==0.6']
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
IS_IN_BED = 'is_in_bed'
SLEEP_NUMBER = 'sleep_number'
SENSOR_TYPES = {
SLEEP_NUMBER: 'SleepNumber',
IS_IN_BED: 'Is In Bed',
}
LEFT = 'left'
RIGHT = 'right'
SIDES = [LEFT, RIGHT]
_LOGGER = logging.getLogger(__name__)
DATA = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup SleepIQ.
Will automatically load sensor components to support
devices discovered on the account.
"""
# pylint: disable=global-statement
global DATA
from sleepyq import Sleepyq
username = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
client = Sleepyq(username, password)
try:
DATA = SleepIQData(client)
DATA.update()
except HTTPError:
message = """
SleepIQ failed to login, double check your username and password"
"""
_LOGGER.error(message)
return False
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
return True
# pylint: disable=too-few-public-methods
class SleepIQData(object):
"""Gets the latest data from SleepIQ."""
def __init__(self, client):
"""Initialize the data object."""
self._client = client
self.beds = {}
self.update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from SleepIQ."""
self._client.login()
beds = self._client.beds_with_sleeper_status()
self.beds = {bed.bed_id: bed for bed in beds}
# pylint: disable=too-few-public-methods, too-many-instance-attributes
class SleepIQSensor(Entity):
"""Implementation of a SleepIQ sensor."""
def __init__(self, sleepiq_data, bed_id, side):
"""Initialize the sensor."""
self._bed_id = bed_id
self._side = side
self.sleepiq_data = sleepiq_data
self.side = None
self.bed = None
# added by subclass
self._name = None
self.type = None
@property
def name(self):
"""Return the name of the sensor."""
return 'SleepNumber {} {} {}'.format(self.bed.name,
self.side.sleeper.first_name,
self._name)
def update(self):
"""Get the latest data from SleepIQ and updates the states."""
# Call the API for new sleepiq data. Each sensor will re-trigger this
# same exact call, but thats fine. We cache results for a short period
# of time to prevent hitting API limits.
self.sleepiq_data.update()
self.bed = self.sleepiq_data.beds[self._bed_id]
self.side = getattr(self.bed, self._side)

View file

@ -433,6 +433,9 @@ slacker==0.9.25
# homeassistant.components.notify.xmpp
sleekxmpp==1.3.1
# homeassistant.components.sleepiq
sleepyq==0.6
# homeassistant.components.media_player.snapcast
snapcast==1.2.2

View file

@ -0,0 +1,50 @@
"""The tests for SleepIQ binary_sensor platform."""
import unittest
from unittest.mock import MagicMock
import requests_mock
from homeassistant import core as ha
from homeassistant.components.binary_sensor import sleepiq
from tests.components.test_sleepiq import mock_responses
class TestSleepIQBinarySensorSetup(unittest.TestCase):
"""Tests the SleepIQ Binary Sensor platform."""
DEVICES = []
def add_devices(self, devices):
"""Mock add devices."""
for device in devices:
self.DEVICES.append(device)
def setUp(self):
"""Initialize values for this testcase class."""
self.hass = ha.HomeAssistant()
self.username = 'foo'
self.password = 'bar'
self.config = {
'username': self.username,
'password': self.password,
}
@requests_mock.Mocker()
def test_setup(self, mock):
"""Test for succesfully setting up the SleepIQ platform."""
mock_responses(mock)
sleepiq.setup_platform(self.hass,
self.config,
self.add_devices,
MagicMock())
self.assertEqual(2, len(self.DEVICES))
left_side = self.DEVICES[1]
self.assertEqual('SleepNumber ILE Test1 Is In Bed', left_side.name)
self.assertEqual('on', left_side.state)
right_side = self.DEVICES[0]
self.assertEqual('SleepNumber ILE Test2 Is In Bed', right_side.name)
self.assertEqual('off', right_side.state)

View file

@ -0,0 +1,50 @@
"""The tests for SleepIQ sensor platform."""
import unittest
from unittest.mock import MagicMock
import requests_mock
from homeassistant import core as ha
from homeassistant.components.sensor import sleepiq
from tests.components.test_sleepiq import mock_responses
class TestSleepIQSensorSetup(unittest.TestCase):
"""Tests the SleepIQ Sensor platform."""
DEVICES = []
def add_devices(self, devices):
"""Mock add devices."""
for device in devices:
self.DEVICES.append(device)
def setUp(self):
"""Initialize values for this testcase class."""
self.hass = ha.HomeAssistant()
self.username = 'foo'
self.password = 'bar'
self.config = {
'username': self.username,
'password': self.password,
}
@requests_mock.Mocker()
def test_setup(self, mock):
"""Test for succesfully setting up the SleepIQ platform."""
mock_responses(mock)
sleepiq.setup_platform(self.hass,
self.config,
self.add_devices,
MagicMock())
self.assertEqual(2, len(self.DEVICES))
left_side = self.DEVICES[1]
self.assertEqual('SleepNumber ILE Test1 SleepNumber', left_side.name)
self.assertEqual(40, left_side.state)
right_side = self.DEVICES[0]
self.assertEqual('SleepNumber ILE Test2 SleepNumber', right_side.name)
self.assertEqual(80, right_side.state)

View file

@ -0,0 +1,75 @@
"""The tests for the SleepIQ component."""
import unittest
import requests_mock
from homeassistant import bootstrap
import homeassistant.components.sleepiq as sleepiq
from tests.common import load_fixture, get_test_home_assistant
def mock_responses(mock):
base_url = 'https://api.sleepiq.sleepnumber.com/rest/'
mock.put(
base_url + 'login',
text=load_fixture('sleepiq-login.json'))
mock.get(
base_url + 'bed?_k=0987',
text=load_fixture('sleepiq-bed.json'))
mock.get(
base_url + 'sleeper?_k=0987',
text=load_fixture('sleepiq-sleeper.json'))
mock.get(
base_url + 'bed/familyStatus?_k=0987',
text=load_fixture('sleepiq-familystatus.json'))
class TestSleepIQ(unittest.TestCase):
"""Tests the SleepIQ component."""
def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
self.username = 'foo'
self.password = 'bar'
self.config = {
'sleepiq': {
'username': self.username,
'password': self.password,
}
}
def tearDown(self): # pylint: disable=invalid-name
"""Stop everything that was started."""
self.hass.stop()
@requests_mock.Mocker()
def test_setup(self, mock):
"""Test the setup."""
mock_responses(mock)
response = sleepiq.setup(self.hass, self.config)
self.assertTrue(response)
@requests_mock.Mocker()
def test_setup_login_failed(self, mock):
"""Test the setup if a bad username or password is given."""
mock.put('https://api.sleepiq.sleepnumber.com/rest/login',
status_code=401,
json=load_fixture('sleepiq-login-failed.json'))
response = sleepiq.setup(self.hass, self.config)
self.assertFalse(response)
def test_setup_component_no_login(self):
"""Test the setup when no login is configured."""
conf = self.config.copy()
del conf['sleepiq']['username']
assert not bootstrap._setup_component(self.hass, sleepiq.DOMAIN, conf)
def test_setup_component_no_password(self):
"""Test the setup when no password is configured."""
conf = self.config.copy()
del conf['sleepiq']['password']
assert not bootstrap._setup_component(self.hass, sleepiq.DOMAIN, conf)

28
tests/fixtures/sleepiq-bed.json vendored Normal file
View file

@ -0,0 +1,28 @@
{
"beds" : [
{
"dualSleep" : true,
"base" : "FlexFit",
"sku" : "AILE",
"model" : "ILE",
"size" : "KING",
"isKidsBed" : false,
"sleeperRightId" : "-80",
"accountId" : "-32",
"bedId" : "-31",
"registrationDate" : "2016-07-22T14:00:58Z",
"serial" : null,
"reference" : "95000794555-1",
"macAddress" : "CD13A384BA51",
"version" : null,
"purchaseDate" : "2016-06-22T00:00:00Z",
"sleeperLeftId" : "-92",
"zipcode" : "12345",
"returnRequestStatus" : 0,
"name" : "ILE",
"status" : 1,
"timezone" : "US/Eastern"
}
]
}

View file

@ -0,0 +1,24 @@
{
"beds" : [
{
"bedId" : "-31",
"rightSide" : {
"alertId" : 0,
"lastLink" : "00:00:00",
"isInBed" : true,
"sleepNumber" : 40,
"alertDetailedMessage" : "No Alert",
"pressure" : -16
},
"status" : 1,
"leftSide" : {
"alertId" : 0,
"lastLink" : "00:00:00",
"sleepNumber" : 80,
"alertDetailedMessage" : "No Alert",
"isInBed" : false,
"pressure" : 2191
}
}
]
}

View file

@ -0,0 +1 @@
{"Error":{"Code":401,"Message":"Authentication token of type [class org.apache.shiro.authc.UsernamePasswordToken] could not be authenticated by any configured realms. Please ensure that at least one realm can authenticate these tokens."}}

7
tests/fixtures/sleepiq-login.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
"edpLoginStatus" : 200,
"userId" : "-42",
"registrationState" : 13,
"key" : "0987",
"edpLoginMessage" : "not used"
}

55
tests/fixtures/sleepiq-sleeper.json vendored Normal file
View file

@ -0,0 +1,55 @@
{
"sleepers" : [
{
"timezone" : "US/Eastern",
"firstName" : "Test1",
"weight" : 150,
"birthMonth" : 12,
"birthYear" : "1990",
"active" : true,
"lastLogin" : "2016-08-26 21:43:27 CDT",
"side" : 1,
"accountId" : "-32",
"height" : 60,
"bedId" : "-31",
"username" : "test1@example.com",
"sleeperId" : "-80",
"avatar" : "",
"emailValidated" : true,
"licenseVersion" : 6,
"duration" : null,
"email" : "test1@example.com",
"isAccountOwner" : true,
"sleepGoal" : 480,
"zipCode" : "12345",
"isChild" : false,
"isMale" : true
},
{
"email" : "test2@example.com",
"duration" : null,
"emailValidated" : true,
"licenseVersion" : 5,
"isChild" : false,
"isMale" : false,
"zipCode" : "12345",
"isAccountOwner" : false,
"sleepGoal" : 480,
"side" : 0,
"lastLogin" : "2016-07-17 15:37:30 CDT",
"birthMonth" : 1,
"birthYear" : "1991",
"active" : true,
"weight" : 151,
"firstName" : "Test2",
"timezone" : "US/Eastern",
"avatar" : "",
"username" : "test2@example.com",
"sleeperId" : "-92",
"bedId" : "-31",
"height" : 65,
"accountId" : "-32"
}
]
}