Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ homeassistant/components/signal_messenger/* @bbernhard
homeassistant/components/simplisafe/* @bachya
homeassistant/components/sinch/* @bendikrb
homeassistant/components/sisyphus/* @jkeljo
homeassistant/components/sleepiq/* @sluzynsk
homeassistant/components/slide/* @ualex73
homeassistant/components/sma/* @kellerza
homeassistant/components/smarthab/* @outadoc
Expand Down
77 changes: 73 additions & 4 deletions homeassistant/components/sleepiq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@
RIGHT = "right"
SIDES = [LEFT, RIGHT]

LEFT_NIGHT_STAND = 1
RIGHT_NIGHT_STAND = 2
RIGHT_NIGHT_LIGHT = 3
LEFT_NIGHT_LIGHT = 4

BED_LIGHTS = {
LEFT_NIGHT_STAND: "Left Night Stand",
RIGHT_NIGHT_STAND: "Right Night Stand",
RIGHT_NIGHT_LIGHT: "Right Night Light",
LEFT_NIGHT_LIGHT: "Left Night Light",
}

SLEEPIQ_COMPONENTS = [
"binary_sensor",
"sensor",
"light",
]

_LOGGER = logging.getLogger(__name__)

DATA = None
Expand All @@ -45,6 +63,9 @@ def setup(hass, config):

Will automatically load sensor components to support
devices discovered on the account.

Will automatically create light components for
nightstand and under bed lights.
"""
global DATA

Expand All @@ -61,8 +82,8 @@ def setup(hass, config):
_LOGGER.error(message)
return False

discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
discovery.load_platform(hass, "binary_sensor", DOMAIN, {}, config)
for component in SLEEPIQ_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)

return True

Expand All @@ -74,8 +95,7 @@ def __init__(self, client):
"""Initialize the data object."""
self._client = client
self.beds = {}

self.update()
self.lights = {}

@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
Expand All @@ -85,6 +105,17 @@ def update(self):

self.beds = {bed.bed_id: bed for bed in beds}

for bed in self.beds:
self.lights = {light: self.get_light(bed, light) for light in BED_LIGHTS}

def set_light(self, bed_id, light, state):
"""Set a light to a new state."""
self._client.set_light(bed_id, light, state)

def get_light(self, bed_id, light):
"""Return current light state."""
return self._client.get_light(bed_id, light)


class SleepIQSensor(Entity):
"""Implementation of a SleepIQ sensor."""
Expand Down Expand Up @@ -117,3 +148,41 @@ def update(self):

self.bed = self.sleepiq_data.beds[self._bed_id]
self.side = getattr(self.bed, self._side)


class SleepIQLight(Entity):
"""Implementation of a SleepIQ Light."""

def __init__(self, sleepiq_data, bed_id, light):
"""Initialize the light."""
self._bed_id = bed_id
self.sleepiq_data = sleepiq_data
self._light = light

self._state = False
self._name = "SleepNumber {} {}".format(
self.sleepiq_data.beds[self._bed_id].name, BED_LIGHTS[light]
)

@property
def name(self):
"""Return the name of the light."""
return self._name

def turn_on(self):
"""Turn on the light."""
self.sleepiq_data.set_light(self._bed_id, self._light, True)

def turn_off(self):
"""Turn off the light."""
self.sleepiq_data.set_light(self._bed_id, self._light, False)

@property
def is_on(self):
"""Ask for state of current light."""
status = self.sleepiq_data.get_light(self._bed_id, self._light)
return status.data["setting"]

def update(self):
"""Get the latest light states from SleepIQ."""
self.sleepiq_data.update()
68 changes: 68 additions & 0 deletions homeassistant/components/sleepiq/light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Support for switching lights on Sleep Number beds off and on.

Creates entities for both night stand lamps and the two
night light (underbed) lights. Night lights cannot be
controlled separately by the stock remote but can be with
this integration.
"""

import logging

from homeassistant.components import sleepiq

_LOGGER = logging.getLogger(__name__)

ICON = "mdi:lamp"


def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the SleepIQ lights."""
if discovery_info is None:
return

data = sleepiq.DATA
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like this will already be up to date in init so you shouldn't need to update again here.

I realize this is existing in init but we shouldn't use globals. You can fix this by storing the data in

hass.data[DOMAIN] = DATA

Then you can access it in setup_platform

DATA = hass.data[DOMAIN]

DATA should probably be renamed as well.

data.update()

dev = list()

for bed_id, bed in data.beds.items(): # pylint: disable=unused-variable
for light in sleepiq.BED_LIGHTS:
dev.append(SleepNumberLight(data, bed_id, light))
add_entities(dev)


class SleepNumberLight(sleepiq.SleepIQLight):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The base class for this should be a Light

from homeassistant.components.light import Light

"""Representation of a SleepIQ Light."""

def __init__(self, sleepiq_data, bed_id, light):
"""Initialize the light."""
sleepiq.SleepIQLight.__init__(self, sleepiq_data, bed_id, light)
Copy link
Copy Markdown
Member

@bdraco bdraco Mar 18, 2020

Choose a reason for hiding this comment

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

Please remove this and pass in what you need from setup_platform


self._light = light
self._state = False
self.type = sleepiq.SLEEP_NUMBER
self._bed_id = bed_id

@property
def state(self):
"""Return state of light."""
return "on" if self._state else "off"

@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON

async def async_turn_on(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
async def async_turn_on(self):
def turn_on(self):

This integration isn't async so you'll need to avoid doing I/O in async.

"""Instruct the light to turn on."""
self.turn_on()

async def async_turn_off(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
async def async_turn_off(self):
def turn_off(self):

"""Instruct the light to turn off."""
self.turn_off()

def update(self):
"""Get the latest data from SleepIQ and updates the states."""
sleepiq.SleepIQLight.update(self)
self._state = self.is_on
2 changes: 1 addition & 1 deletion homeassistant/components/sleepiq/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/integrations/sleepiq",
"requirements": ["sleepyq==0.7"],
"dependencies": [],
"codeowners": []
"codeowners": ["@sluzynsk"]
}
27 changes: 26 additions & 1 deletion tests/components/sleepiq/test_init.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""The tests for the SleepIQ component."""
import unittest
from unittest.mock import MagicMock, patch

from asynctest import MagicMock, patch
import requests_mock

from homeassistant import setup
Expand All @@ -26,6 +26,31 @@ def mock_responses(mock, single=False):
base_url + "bed/familyStatus?_k=0987",
text=load_fixture("sleepiq-familystatus{}.json".format(suffix)),
)
mock.get(
base_url + "bed/-31/foundation/outlet?_k=0987&outletId=1",
text=load_fixture("sleepiq-light-1.json"),
)
mock.get(
base_url + "bed/-31/foundation/outlet?_k=0987&outletId=2",
text=load_fixture("sleepiq-light-2.json"),
)
mock.get(
base_url + "bed/-31/foundation/outlet?_k=0987&outletId=3",
text=load_fixture("sleepiq-light-3.json"),
)
mock.get(
base_url + "bed/-31/foundation/outlet?_k=0987&outletId=4",
text=load_fixture("sleepiq-light-4.json"),
)
# Test not logged in request
mock.get(
base_url + "bed/-31/foundation/outlet?outletId=1",
text=load_fixture("sleepiq-light-1.json"),
)
mock.put(
base_url + "bed/-31/foundation/outlet?_k=0987",
text="{ 'outletId': '1', 'setting': '1' }",
)


class TestSleepIQ(unittest.TestCase):
Expand Down
127 changes: 127 additions & 0 deletions tests/components/sleepiq/test_light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""The tests for SleepIQ light platform."""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for adding tests!!!

Please remove the classes from the tests. A good example of how tests should be written is tests/components/wled

import asyncio
import unittest
from unittest.mock import MagicMock

import requests_mock

import homeassistant.components.sleepiq.light as sleepiq
from homeassistant.setup import setup_component

from tests.common import get_test_home_assistant
from tests.components.sleepiq.test_init import mock_responses


class TestSleepIQLightSetup(unittest.TestCase):
"""Tests the SleepIQ Light platform."""

DEVICES = []

def add_entities(self, devices):
"""Mock add devices."""
for device in devices:
self.DEVICES.append(device)

def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
self.username = "foo"
self.password = "bar"
self.config = {"username": self.username, "password": self.password}
self.DEVICES = []

def tearDown(self): # pylint: disable=invalid-name
"""Stop everything that was started."""
self.hass.stop()

@requests_mock.Mocker()
def test_setup(self, mock):
"""Test for successfully setting up the SleepIQ platform."""
mock_responses(mock)

assert setup_component(self.hass, "sleepiq", {"sleepiq": self.config})

sleepiq.setup_platform(self.hass, self.config, self.add_entities, None)
assert not len(self.DEVICES)

sleepiq.setup_platform(self.hass, self.config, self.add_entities, MagicMock())
assert 4 == len(self.DEVICES)

left_night_stand = self.DEVICES[0]
assert "SleepNumber ILE Left Night Stand" == left_night_stand.name
assert "off" == left_night_stand.state

right_night_stand = self.DEVICES[1]
assert "SleepNumber ILE Right Night Stand" == right_night_stand.name
assert "off" == right_night_stand.state

right_night_light = self.DEVICES[2]
assert "SleepNumber ILE Right Night Light" == right_night_light.name
assert "off" == right_night_light.state

left_night_light = self.DEVICES[3]
assert "SleepNumber ILE Left Night Light" == left_night_light.name
assert "off" == left_night_light.state


class TestSleepIQLight(unittest.TestCase):
"""Tests for functionality of the lights platform."""

DEVICES = []

def add_entities(self, devices):
"""Mock add devices."""
for device in devices:
self.DEVICES.append(device)

def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
self.username = "foo"
self.password = "bar"
self.config = {"username": self.username, "password": self.password}
self.DEVICES = []

def tearDown(self): # pylint: disable=invalid-name
"""Stop everything that was started."""
self.hass.stop()

@requests_mock.Mocker()
def test_turn_on(self, mock):
"""Test turning on a light."""
mock_responses(mock)

sleepiq.setup_platform(self.hass, self.config, self.add_entities, MagicMock())

left_night_stand = self.DEVICES[0]
asyncio.run_coroutine_threadsafe(
sleepiq.SleepNumberLight.async_turn_on(left_night_stand), self.hass.loop
)

assert left_night_stand.state is not None

@requests_mock.Mocker()
def test_turn_off(self, mock):
"""Test turning off a light."""
mock_responses(mock)

sleepiq.setup_platform(self.hass, self.config, self.add_entities, MagicMock())

left_night_stand = self.DEVICES[0]

asyncio.run_coroutine_threadsafe(
sleepiq.SleepNumberLight.async_turn_off(left_night_stand), self.hass.loop
)

assert left_night_stand.state == "off"

@requests_mock.Mocker()
def test_update(self, mock):
"""Test that the update function leaves the lights with a measureable state."""
mock_responses(mock)

sleepiq.setup_platform(self.hass, self.config, self.add_entities, MagicMock())

left_night_stand = self.DEVICES[0]
sleepiq.SleepNumberLight.update(left_night_stand)
assert left_night_stand.state is not None
6 changes: 6 additions & 0 deletions tests/fixtures/sleepiq-light-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"bedId": "-31",
"outlet": 1,
"setting": 0,
"timer": null
}
6 changes: 6 additions & 0 deletions tests/fixtures/sleepiq-light-2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"bedId": "-31",
"outlet": 1,
"setting": 0,
"timer": null
}
6 changes: 6 additions & 0 deletions tests/fixtures/sleepiq-light-3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"bedId": "-31",
"outlet": 1,
"setting": 0,
"timer": null
}
Loading