SleepIQ component with sensor and binary sensor platforms (#3390)
Original from #2949
This commit is contained in:
parent
de2eed3c9f
commit
1697a8c774
12 changed files with 540 additions and 0 deletions
59
homeassistant/components/binary_sensor/sleepiq.py
Normal file
59
homeassistant/components/binary_sensor/sleepiq.py
Normal 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
|
58
homeassistant/components/sensor/sleepiq.py
Normal file
58
homeassistant/components/sensor/sleepiq.py
Normal 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
|
130
homeassistant/components/sleepiq.py
Normal file
130
homeassistant/components/sleepiq.py
Normal 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)
|
|
@ -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
|
||||
|
||||
|
|
50
tests/components/binary_sensor/test_sleepiq.py
Normal file
50
tests/components/binary_sensor/test_sleepiq.py
Normal 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)
|
50
tests/components/sensor/test_sleepiq.py
Normal file
50
tests/components/sensor/test_sleepiq.py
Normal 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)
|
75
tests/components/test_sleepiq.py
Normal file
75
tests/components/test_sleepiq.py
Normal 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
28
tests/fixtures/sleepiq-bed.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
24
tests/fixtures/sleepiq-familystatus.json
vendored
Normal file
24
tests/fixtures/sleepiq-familystatus.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
1
tests/fixtures/sleepiq-login-failed.json
vendored
Normal file
1
tests/fixtures/sleepiq-login-failed.json
vendored
Normal 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
7
tests/fixtures/sleepiq-login.json
vendored
Normal 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
55
tests/fixtures/sleepiq-sleeper.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue