Convert Nanoleaf integration to use Async library aionanoleaf (#56548)

This commit is contained in:
Milan Meulemans 2021-09-23 22:37:37 +02:00 committed by GitHub
parent 7ece35cd6f
commit 0b53f73fe2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 127 additions and 136 deletions

View file

@ -695,7 +695,6 @@ omit =
homeassistant/components/nad/media_player.py
homeassistant/components/nanoleaf/__init__.py
homeassistant/components/nanoleaf/light.py
homeassistant/components/nanoleaf/util.py
homeassistant/components/neato/__init__.py
homeassistant/components/neato/api.py
homeassistant/components/neato/camera.py

View file

@ -1,21 +1,22 @@
"""The Nanoleaf integration."""
from pynanoleaf.pynanoleaf import InvalidToken, Nanoleaf, Unavailable
from aionanoleaf import InvalidToken, Nanoleaf, Unavailable
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEVICE, DOMAIN, NAME, SERIAL_NO
from .util import pynanoleaf_get_info
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Nanoleaf from a config entry."""
nanoleaf = Nanoleaf(entry.data[CONF_HOST])
nanoleaf.token = entry.data[CONF_TOKEN]
nanoleaf = Nanoleaf(
async_get_clientsession(hass), entry.data[CONF_HOST], entry.data[CONF_TOKEN]
)
try:
info = await hass.async_add_executor_job(pynanoleaf_get_info, nanoleaf)
await nanoleaf.get_info()
except Unavailable as err:
raise ConfigEntryNotReady from err
except InvalidToken as err:
@ -23,8 +24,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
DEVICE: nanoleaf,
NAME: info["name"],
SERIAL_NO: info["serialNo"],
NAME: nanoleaf.name,
SERIAL_NO: nanoleaf.serial_no,
}
hass.async_create_task(

View file

@ -5,17 +5,17 @@ import logging
import os
from typing import Any, Final, cast
from pynanoleaf import InvalidToken, Nanoleaf, NotAuthorizingNewTokens, Unavailable
from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.util.json import load_json, save_json
from .const import DOMAIN
from .util import pynanoleaf_get_info
_LOGGER = logging.getLogger(__name__)
@ -53,9 +53,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=USER_SCHEMA, last_step=False
)
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
self.nanoleaf = Nanoleaf(user_input[CONF_HOST])
self.nanoleaf = Nanoleaf(
async_get_clientsession(self.hass), user_input[CONF_HOST]
)
try:
await self.hass.async_add_executor_job(self.nanoleaf.authorize)
await self.nanoleaf.authorize()
except Unavailable:
return self.async_show_form(
step_id="user",
@ -63,7 +65,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors={"base": "cannot_connect"},
last_step=False,
)
except NotAuthorizingNewTokens:
except Unauthorized:
pass
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error connecting to Nanoleaf")
@ -81,7 +83,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
config_entries.ConfigEntry,
self.hass.config_entries.async_get_entry(self.context["entry_id"]),
)
self.nanoleaf = Nanoleaf(data[CONF_HOST])
self.nanoleaf = Nanoleaf(async_get_clientsession(self.hass), data[CONF_HOST])
self.context["title_placeholders"] = {"name": self.reauth_entry.title}
return await self.async_step_link()
@ -106,7 +108,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
name = discovery_info["name"].replace(f".{discovery_info['type']}", "")
await self.async_set_unique_id(name)
self._abort_if_unique_id_configured({CONF_HOST: host})
self.nanoleaf = Nanoleaf(host)
# Import from discovery integration
self.device_id = discovery_info["properties"]["id"]
@ -116,16 +117,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
load_json, self.hass.config.path(CONFIG_FILE)
),
)
self.nanoleaf.token = self.discovery_conf.get(self.device_id, {}).get(
auth_token: str | None = self.discovery_conf.get(self.device_id, {}).get(
"token", # >= 2021.4
self.discovery_conf.get(host, {}).get("token"), # < 2021.4
)
if self.nanoleaf.token is not None:
if auth_token is not None:
self.nanoleaf = Nanoleaf(
async_get_clientsession(self.hass), host, auth_token
)
_LOGGER.warning(
"Importing Nanoleaf %s from the discovery integration", name
)
return await self.async_setup_finish(discovery_integration_import=True)
self.nanoleaf = Nanoleaf(async_get_clientsession(self.hass), host)
self.context["title_placeholders"] = {"name": name}
return await self.async_step_link()
@ -137,8 +141,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="link")
try:
await self.hass.async_add_executor_job(self.nanoleaf.authorize)
except NotAuthorizingNewTokens:
await self.nanoleaf.authorize()
except Unauthorized:
return self.async_show_form(
step_id="link", errors={"base": "not_allowing_new_tokens"}
)
@ -153,7 +157,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.reauth_entry,
data={
**self.reauth_entry.data,
CONF_TOKEN: self.nanoleaf.token,
CONF_TOKEN: self.nanoleaf.auth_token,
},
)
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
@ -167,8 +171,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.debug(
"Importing Nanoleaf on %s from your configuration.yaml", config[CONF_HOST]
)
self.nanoleaf = Nanoleaf(config[CONF_HOST])
self.nanoleaf.token = config[CONF_TOKEN]
self.nanoleaf = Nanoleaf(
async_get_clientsession(self.hass), config[CONF_HOST], config[CONF_TOKEN]
)
return await self.async_setup_finish()
async def async_setup_finish(
@ -176,9 +181,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Finish Nanoleaf config flow."""
try:
info = await self.hass.async_add_executor_job(
pynanoleaf_get_info, self.nanoleaf
)
await self.nanoleaf.get_info()
except Unavailable:
return self.async_abort(reason="cannot_connect")
except InvalidToken:
@ -188,7 +191,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"Unknown error connecting with Nanoleaf at %s", self.nanoleaf.host
)
return self.async_abort(reason="unknown")
name = info["name"]
name = self.nanoleaf.name
await self.async_set_unique_id(name)
self._abort_if_unique_id_configured({CONF_HOST: self.nanoleaf.host})
@ -215,6 +218,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
title=name,
data={
CONF_HOST: self.nanoleaf.host,
CONF_TOKEN: self.nanoleaf.token,
CONF_TOKEN: self.nanoleaf.auth_token,
},
)

View file

@ -3,7 +3,8 @@ from __future__ import annotations
import logging
from pynanoleaf import Unavailable
from aiohttp import ServerDisconnectedError
from aionanoleaf import Unavailable
import voluptuous as vol
from homeassistant.components.light import (
@ -153,7 +154,7 @@ class NanoleafLight(LightEntity):
@property
def is_on(self):
"""Return true if light is on."""
return self._state
return self._light.is_on
@property
def hs_color(self):
@ -165,7 +166,7 @@ class NanoleafLight(LightEntity):
"""Flag supported features."""
return SUPPORT_NANOLEAF
def turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Instruct the light to turn on."""
brightness = kwargs.get(ATTR_BRIGHTNESS)
hs_color = kwargs.get(ATTR_HS_COLOR)
@ -175,44 +176,51 @@ class NanoleafLight(LightEntity):
if hs_color:
hue, saturation = hs_color
self._light.hue = int(hue)
self._light.saturation = int(saturation)
await self._light.set_hue(int(hue))
await self._light.set_saturation(int(saturation))
if color_temp_mired:
self._light.color_temperature = mired_to_kelvin(color_temp_mired)
await self._light.set_color_temperature(mired_to_kelvin(color_temp_mired))
if transition:
if brightness: # tune to the required brightness in n seconds
self._light.brightness_transition(
int(brightness / 2.55), int(transition)
await self._light.set_brightness(
int(brightness / 2.55), transition=int(kwargs[ATTR_TRANSITION])
)
else: # If brightness is not specified, assume full brightness
self._light.brightness_transition(100, int(transition))
await self._light.set_brightness(
100, transition=int(kwargs[ATTR_TRANSITION])
)
else: # If no transition is occurring, turn on the light
self._light.on = True
await self._light.turn_on()
if brightness:
self._light.brightness = int(brightness / 2.55)
await self._light.set_brightness(int(brightness / 2.55))
if effect:
if effect not in self._effects_list:
raise ValueError(
f"Attempting to apply effect not in the effect list: '{effect}'"
)
self._light.effect = effect
await self._light.set_effect(effect)
def turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
transition = kwargs.get(ATTR_TRANSITION)
if transition:
self._light.brightness_transition(0, int(transition))
await self._light.set_brightness(0, transition=int(transition))
else:
self._light.on = False
await self._light.turn_off()
def update(self):
async def async_update(self) -> None:
"""Fetch new state data for this light."""
try:
self._available = self._light.available
await self._light.get_info()
except ServerDisconnectedError:
# Retry the request once if the device disconnected
await self._light.get_info()
except Unavailable:
self._available = False
return
self._available = True
self._brightness = self._light.brightness
self._effects_list = self._light.effects
self._effects_list = self._light.effects_list
# Nanoleaf api returns non-existent effect named "*Solid*" when light set to solid color.
# This causes various issues with scening (see https://github.com/home-assistant/core/issues/36359).
# Until fixed at the library level, we should ensure the effect exists before saving to light properties
@ -225,7 +233,4 @@ class NanoleafLight(LightEntity):
else:
self._color_temp = None
self._hs_color = None
self._state = self._light.on
except Unavailable as err:
_LOGGER.error("Could not update status for %s (%s)", self.name, err)
self._available = False
self._state = self._light.is_on

View file

@ -3,7 +3,7 @@
"name": "Nanoleaf",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nanoleaf",
"requirements": ["pynanoleaf==0.1.0"],
"requirements": ["aionanoleaf==0.0.1"],
"zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."],
"homekit" : {
"models": [

View file

@ -1,7 +0,0 @@
"""Nanoleaf integration util."""
from pynanoleaf.pynanoleaf import Nanoleaf
def pynanoleaf_get_info(nanoleaf_light: Nanoleaf) -> dict:
"""Get Nanoleaf light info."""
return nanoleaf_light.info

View file

@ -218,6 +218,9 @@ aiomodernforms==0.1.8
# homeassistant.components.yamaha_musiccast
aiomusiccast==0.9.2
# homeassistant.components.nanoleaf
aionanoleaf==0.0.1
# homeassistant.components.keyboard_remote
aionotify==0.2.0
@ -1649,9 +1652,6 @@ pymyq==3.1.4
# homeassistant.components.mysensors
pymysensors==0.21.0
# homeassistant.components.nanoleaf
pynanoleaf==0.1.0
# homeassistant.components.nello
pynello==2.0.3

View file

@ -145,6 +145,9 @@ aiomodernforms==0.1.8
# homeassistant.components.yamaha_musiccast
aiomusiccast==0.9.2
# homeassistant.components.nanoleaf
aionanoleaf==0.0.1
# homeassistant.components.notion
aionotion==3.0.2
@ -968,9 +971,6 @@ pymyq==3.1.4
# homeassistant.components.mysensors
pymysensors==0.21.0
# homeassistant.components.nanoleaf
pynanoleaf==0.1.0
# homeassistant.components.netgear
pynetgear==0.7.0

View file

@ -1,10 +1,9 @@
"""Test the Nanoleaf config flow."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
from pynanoleaf import InvalidToken, NotAuthorizingNewTokens, Unavailable
from pynanoleaf.pynanoleaf import NanoleafError
from aionanoleaf import InvalidToken, NanoleafException, Unauthorized, Unavailable
import pytest
from homeassistant import config_entries
@ -23,6 +22,21 @@ TEST_DEVICE_ID = "5E:2E:EA:XX:XX:XX"
TEST_OTHER_DEVICE_ID = "5E:2E:EA:YY:YY:YY"
def _mock_nanoleaf(
host: str = TEST_HOST,
auth_token: str = TEST_TOKEN,
authorize_error: Exception | None = None,
get_info_error: Exception | None = None,
):
nanoleaf = MagicMock()
nanoleaf.name = TEST_NAME
nanoleaf.host = host
nanoleaf.auth_token = auth_token
nanoleaf.authorize = AsyncMock(side_effect=authorize_error)
nanoleaf.get_info = AsyncMock(side_effect=get_info_error)
return nanoleaf
async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None:
"""Test we handle Unavailable in user and link step."""
result = await hass.config_entries.flow.async_init(
@ -30,7 +44,7 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None
)
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize",
side_effect=Unavailable("message"),
side_effect=Unavailable,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -58,7 +72,7 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize",
side_effect=Unavailable("message"),
side_effect=Unavailable,
):
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -71,8 +85,8 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None
@pytest.mark.parametrize(
"error, reason",
[
(Unavailable("message"), "cannot_connect"),
(InvalidToken("message"), "invalid_token"),
(Unavailable, "cannot_connect"),
(InvalidToken, "invalid_token"),
(Exception, "unknown"),
],
)
@ -85,7 +99,6 @@ async def test_user_error_setup_finish(
)
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize",
return_value=None,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -98,9 +111,8 @@ async def test_user_error_setup_finish(
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize",
return_value=None,
), patch(
"homeassistant.components.nanoleaf.config_flow.pynanoleaf_get_info",
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info",
side_effect=error,
):
result3 = await hass.config_entries.flow.async_configure(
@ -117,19 +129,10 @@ async def test_user_not_authorizing_new_tokens_user_step_link_step(
"""Test we handle NotAuthorizingNewTokens in user step and link step."""
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf",
return_value=_mock_nanoleaf(authorize_error=Unauthorized()),
) as mock_nanoleaf, patch(
"homeassistant.components.nanoleaf.config_flow.pynanoleaf_get_info",
return_value={"name": TEST_NAME},
), patch(
"homeassistant.components.nanoleaf.async_setup_entry", return_value=True
) as mock_setup_entry:
nanoleaf = mock_nanoleaf.return_value
nanoleaf.authorize.side_effect = NotAuthorizingNewTokens(
"Not authorizing new tokens"
)
nanoleaf.host = TEST_HOST
nanoleaf.token = TEST_TOKEN
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@ -160,8 +163,7 @@ async def test_user_not_authorizing_new_tokens_user_step_link_step(
assert result4["errors"] == {"base": "not_allowing_new_tokens"}
assert result4["step_id"] == "link"
nanoleaf.authorize.side_effect = None
nanoleaf.authorize.return_value = None
mock_nanoleaf.return_value.authorize.side_effect = None
result5 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -183,8 +185,8 @@ async def test_user_exception_user_step(hass: HomeAssistant) -> None:
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize",
side_effect=Exception,
"homeassistant.components.nanoleaf.config_flow.Nanoleaf",
return_value=_mock_nanoleaf(authorize_error=Exception()),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -198,9 +200,9 @@ async def test_user_exception_user_step(hass: HomeAssistant) -> None:
assert not result2["last_step"]
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize",
return_value=None,
):
"homeassistant.components.nanoleaf.config_flow.Nanoleaf",
return_value=_mock_nanoleaf(),
) as mock_nanoleaf:
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@ -209,10 +211,8 @@ async def test_user_exception_user_step(hass: HomeAssistant) -> None:
)
assert result3["step_id"] == "link"
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize",
side_effect=Exception,
):
mock_nanoleaf.return_value.authorize.side_effect = Exception()
result4 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
@ -221,13 +221,8 @@ async def test_user_exception_user_step(hass: HomeAssistant) -> None:
assert result4["step_id"] == "link"
assert result4["errors"] == {"base": "unknown"}
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize",
return_value=None,
), patch(
"homeassistant.components.nanoleaf.config_flow.pynanoleaf_get_info",
side_effect=Exception,
):
mock_nanoleaf.return_value.authorize.side_effect = None
mock_nanoleaf.return_value.get_info.side_effect = Exception()
result5 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
@ -249,8 +244,7 @@ async def test_discovery_link_unavailable(
) -> None:
"""Test discovery and abort if device is unavailable."""
with patch(
"homeassistant.components.nanoleaf.config_flow.pynanoleaf_get_info",
return_value={"name": TEST_NAME},
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info",
), patch(
"homeassistant.components.nanoleaf.config_flow.load_json",
return_value={},
@ -278,7 +272,7 @@ async def test_discovery_link_unavailable(
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize",
side_effect=Unavailable("message"),
side_effect=Unavailable,
):
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == "abort"
@ -287,10 +281,6 @@ async def test_discovery_link_unavailable(
async def test_reauth(hass: HomeAssistant) -> None:
"""Test Nanoleaf reauth flow."""
nanoleaf = MagicMock()
nanoleaf.host = TEST_HOST
nanoleaf.token = TEST_TOKEN
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=TEST_NAME,
@ -300,7 +290,7 @@ async def test_reauth(hass: HomeAssistant) -> None:
with patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf",
return_value=nanoleaf,
return_value=_mock_nanoleaf(),
), patch(
"homeassistant.components.nanoleaf.async_setup_entry",
return_value=True,
@ -331,8 +321,8 @@ async def test_reauth(hass: HomeAssistant) -> None:
async def test_import_config(hass: HomeAssistant) -> None:
"""Test configuration import."""
with patch(
"homeassistant.components.nanoleaf.config_flow.pynanoleaf_get_info",
return_value={"name": TEST_NAME},
"homeassistant.components.nanoleaf.config_flow.Nanoleaf",
return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN),
), patch(
"homeassistant.components.nanoleaf.async_setup_entry",
return_value=True,
@ -355,17 +345,17 @@ async def test_import_config(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
"error, reason",
[
(Unavailable("message"), "cannot_connect"),
(InvalidToken("message"), "invalid_token"),
(Unavailable, "cannot_connect"),
(InvalidToken, "invalid_token"),
(Exception, "unknown"),
],
)
async def test_import_config_error(
hass: HomeAssistant, error: NanoleafError, reason: str
hass: HomeAssistant, error: NanoleafException, reason: str
) -> None:
"""Test configuration import with errors in setup_finish."""
with patch(
"homeassistant.components.nanoleaf.config_flow.pynanoleaf_get_info",
"homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info",
side_effect=error,
):
result = await hass.config_entries.flow.async_init(
@ -432,8 +422,8 @@ async def test_import_discovery_integration(
"homeassistant.components.nanoleaf.config_flow.load_json",
return_value=dict(nanoleaf_conf_file),
), patch(
"homeassistant.components.nanoleaf.config_flow.pynanoleaf_get_info",
return_value={"name": TEST_NAME},
"homeassistant.components.nanoleaf.config_flow.Nanoleaf",
return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN),
), patch(
"homeassistant.components.nanoleaf.config_flow.save_json",
return_value=None,