Merge pull request #57455 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-10-10 21:14:45 -07:00 committed by GitHub
commit a3ae25efdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 406 additions and 59 deletions

View File

@ -276,6 +276,10 @@ class DHCPWatcher(WatcherBase):
self._sniffer.stop()
async def async_start(self):
"""Start watching for dhcp packets."""
await self.hass.async_add_executor_job(self._start)
def _start(self):
"""Start watching for dhcp packets."""
# Local import because importing from scapy has side effects such as opening
# sockets
@ -319,7 +323,7 @@ class DHCPWatcher(WatcherBase):
conf.sniff_promisc = 0
try:
await self.hass.async_add_executor_job(_verify_l2socket_setup, FILTER)
_verify_l2socket_setup(FILTER)
except (Scapy_Exception, OSError) as ex:
if os.geteuid() == 0:
_LOGGER.error("Cannot watch for dhcp packets: %s", ex)
@ -330,7 +334,7 @@ class DHCPWatcher(WatcherBase):
return
try:
await self.hass.async_add_executor_job(_verify_working_pcap, FILTER)
_verify_working_pcap(FILTER)
except (Scapy_Exception, ImportError) as ex:
_LOGGER.error(
"Cannot watch for dhcp packets without a functional packet filter: %s",

View File

@ -1,7 +1,9 @@
"""Support for Efergy sensors."""
from __future__ import annotations
from pyefergy import Efergy
import logging
from pyefergy import Efergy, exceptions
import voluptuous as vol
from homeassistant.components.sensor import (
@ -20,6 +22,7 @@ from homeassistant.const import (
POWER_WATT,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -39,6 +42,8 @@ CONF_CURRENT_VALUES = "current_values"
DEFAULT_PERIOD = "year"
DEFAULT_UTC_OFFSET = "0"
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES: dict[str, SensorEntityDescription] = {
CONF_INSTANT: SensorEntityDescription(
key=CONF_INSTANT,
@ -102,7 +107,10 @@ async def async_setup_platform(
)
dev = []
try:
sensors = await api.get_sids()
except (exceptions.DataError, exceptions.ConnectTimeout) as ex:
raise PlatformNotReady("Error getting data from Efergy:") from ex
for variable in config[CONF_MONITORED_VARIABLES]:
if variable[CONF_TYPE] == CONF_CURRENT_VALUES:
for sensor in sensors:
@ -150,6 +158,15 @@ class EfergySensor(SensorEntity):
async def async_update(self) -> None:
"""Get the Efergy monitor data from the web service."""
try:
self._attr_native_value = await self.api.async_get_reading(
self.entity_description.key, period=self.period, sid=self.sid
)
except (exceptions.DataError, exceptions.ConnectTimeout) as ex:
if self._attr_available:
self._attr_available = False
_LOGGER.error("Error getting data from Efergy: %s", ex)
return
if not self._attr_available:
self._attr_available = True
_LOGGER.info("Connection to Efergy has resumed")

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20211007.0"
"home-assistant-frontend==20211007.1"
],
"dependencies": [
"api",

View File

@ -205,21 +205,18 @@ async def async_setup_entry(
# Load platforms for the devices in the ISY controller that we support.
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
def _start_auto_update() -> None:
"""Start isy auto update."""
_LOGGER.debug("ISY Starting Event Stream and automatic updates")
isy.websocket.start()
def _stop_auto_update(event) -> None:
@callback
def _async_stop_auto_update(event) -> None:
"""Stop the isy auto update on Home Assistant Shutdown."""
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
isy.websocket.stop()
await hass.async_add_executor_job(_start_auto_update)
_LOGGER.debug("ISY Starting Event Stream and automatic updates")
isy.websocket.start()
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_auto_update)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update)
)
# Register Integration-wide Services:

View File

@ -35,7 +35,7 @@ DEVICE_ICONS = {
0: "mdi:access-point-network", # Router (Orbi ...)
1: "mdi:book-open-variant", # Amazon Kindle
2: "mdi:android", # Android Device
3: "mdi:cellphone-android", # Android Phone
3: "mdi:cellphone", # Android Phone
4: "mdi:tablet-android", # Android Tablet
5: "mdi:router-wireless", # Apple Airport Express
6: "mdi:disc-player", # Blu-ray Player
@ -46,15 +46,15 @@ DEVICE_ICONS = {
11: "mdi:play-network", # DVR
12: "mdi:gamepad-variant", # Gaming Console
13: "mdi:desktop-mac", # iMac
14: "mdi:tablet-ipad", # iPad
15: "mdi:tablet-ipad", # iPad Mini
16: "mdi:cellphone-iphone", # iPhone 5/5S/5C
17: "mdi:cellphone-iphone", # iPhone
14: "mdi:tablet", # iPad
15: "mdi:tablet", # iPad Mini
16: "mdi:cellphone", # iPhone 5/5S/5C
17: "mdi:cellphone", # iPhone
18: "mdi:ipod", # iPod Touch
19: "mdi:linux", # Linux PC
20: "mdi:apple-finder", # Mac Mini
21: "mdi:desktop-tower", # Mac Pro
22: "mdi:laptop-mac", # MacBook
22: "mdi:laptop", # MacBook
23: "mdi:play-network", # Media Device
24: "mdi:network", # Network Device
25: "mdi:play-network", # Other STB
@ -71,7 +71,7 @@ DEVICE_ICONS = {
36: "mdi:tablet", # Tablet
37: "mdi:desktop-classic", # UNIX PC
38: "mdi:desktop-tower-monitor", # Windows PC
39: "mdi:laptop-windows", # Surface
39: "mdi:laptop", # Surface
40: "mdi:access-point-network", # Wifi Extender
41: "mdi:apple-airplay", # Apple TV
41: "mdi:cast-variant", # Apple TV
}

View File

@ -3,7 +3,7 @@
"name": "National Weather Service (NWS)",
"documentation": "https://www.home-assistant.io/integrations/nws",
"codeowners": ["@MatthewFlamm"],
"requirements": ["pynws==1.3.1"],
"requirements": ["pynws==1.3.2"],
"quality_scale": "platinum",
"config_flow": true,
"iot_class": "cloud_polling"

View File

@ -156,8 +156,9 @@ def register_services(hass):
vol.Required(ATTR_GW_ID): vol.All(
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
),
vol.Optional(ATTR_DATE, default=date.today()): cv.date,
vol.Optional(ATTR_TIME, default=datetime.now().time()): cv.time,
# pylint: disable=unnecessary-lambda
vol.Optional(ATTR_DATE, default=lambda: date.today()): cv.date,
vol.Optional(ATTR_TIME, default=lambda: datetime.now().time()): cv.time,
}
)
service_set_control_setpoint_schema = vol.Schema(

View File

@ -54,7 +54,7 @@ set_clock:
selector:
text:
time:
name: Name
name: Time
description: Optional time in 24h format which will be provided to the thermostat. Defaults to the current time.
example: "19:34"
selector:

View File

@ -275,7 +275,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
if block.type != "device":
continue
if block.wakeupEvent[0] == "button":
if len(block.wakeupEvent) == 1 and block.wakeupEvent[0] == "button":
self._last_input_events_count[1] = -1
break

View File

@ -21,6 +21,11 @@ LIGHT_TRANSITION_MIN_FIRMWARE_DATE: Final = 20210226
# max light transition time in milliseconds
MAX_TRANSITION_TIME: Final = 5000
RGBW_MODELS: Final = (
"SHBLB-1",
"SHRGBW2",
)
MODELS_SUPPORTING_LIGHT_TRANSITION: Final = (
"SHBDUO-1",
"SHCB-1",

View File

@ -46,6 +46,7 @@ from .const import (
LIGHT_TRANSITION_MIN_FIRMWARE_DATE,
MAX_TRANSITION_TIME,
MODELS_SUPPORTING_LIGHT_TRANSITION,
RGBW_MODELS,
RPC,
SHBLB_1_RGB_EFFECTS,
STANDARD_RGB_EFFECTS,
@ -143,7 +144,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"):
self._min_kelvin = KELVIN_MIN_VALUE_COLOR
if hasattr(block, "white"):
if wrapper.model in RGBW_MODELS:
self._supported_color_modes.add(COLOR_MODE_RGBW)
else:
self._supported_color_modes.add(COLOR_MODE_RGB)

View File

@ -133,6 +133,10 @@ def is_block_momentary_input(settings: dict[str, Any], block: Block) -> bool:
if settings["device"]["type"] in SHBTN_MODELS:
return True
if settings.get("mode") == "roller":
button_type = settings["rollers"][0]["button_type"]
return button_type in ["momentary", "momentary_on_release"]
button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
if button is None:
return False

View File

@ -2,7 +2,7 @@
"domain": "switchbot",
"name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.11.0"],
"requirements": ["PySwitchbot==0.12.0"],
"config_flow": true,
"codeowners": ["@danielhiversen", "@RenierM26"],
"iot_class": "local_polling"

View File

@ -11,6 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import network
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady
from homeassistant.const import (
CONF_HOST,
@ -19,7 +20,11 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
@ -158,12 +163,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except SmartDeviceException as ex:
raise ConfigEntryNotReady from ex
if device.is_dimmer:
async_fix_dimmer_unique_id(hass, entry, device)
hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
@callback
def async_fix_dimmer_unique_id(
hass: HomeAssistant, entry: ConfigEntry, device: SmartDevice
) -> None:
"""Migrate the unique id of dimmers back to the legacy one.
Dimmers used to use the switch format since pyHS100 treated them as SmartPlug but
the old code created them as lights
https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86
"""
# This is the unique id before 2021.0/2021.1
original_unique_id = legacy_device_id(device)
# This is the unique id that was used in 2021.0/2021.1 rollout
rollout_unique_id = device.mac.replace(":", "").upper()
entity_registry = er.async_get(hass)
rollout_entity_id = entity_registry.async_get_entity_id(
LIGHT_DOMAIN, DOMAIN, rollout_unique_id
)
original_entry_id = entity_registry.async_get_entity_id(
LIGHT_DOMAIN, DOMAIN, original_unique_id
)
# If they are now using the 2021.0/2021.1 rollout entity id
# and have deleted the original entity id, we want to update that entity id
# so they don't end up with another _2 entity, but only if they deleted
# the original
if rollout_entity_id and not original_entry_id:
entity_registry.async_update_entity(
rollout_entity_id, new_unique_id=original_unique_id
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hass_data: dict[str, Any] = hass.data[DOMAIN]

View File

@ -26,6 +26,7 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin as mired_to_kelvin,
)
from . import legacy_device_id
from .const import DOMAIN
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import CoordinatedTPLinkEntity, async_refresh_after
@ -58,6 +59,13 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
"""Initialize the switch."""
super().__init__(device, coordinator)
# For backwards compat with pyHS100
if self.device.is_dimmer:
# Dimmers used to use the switch format since
# pyHS100 treated them as SmartPlug but the old code
# created them as lights
# https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86
self._attr_unique_id = legacy_device_id(device)
else:
self._attr_unique_id = self.device.mac.replace(":", "").upper()
@async_refresh_after

View File

@ -3,7 +3,7 @@
"name": "Version",
"documentation": "https://www.home-assistant.io/integrations/version",
"requirements": [
"pyhaversion==21.7.0"
"pyhaversion==21.10.0"
],
"codeowners": [
"@fabaff",

View File

@ -36,8 +36,8 @@ from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
STATE_CHANGE_TIME = 0.25 # seconds
STATE_CHANGE_TIME = 0.40 # seconds
POWER_STATE_CHANGE_TIME = 1 # seconds
DOMAIN = "yeelight"
DATA_YEELIGHT = DOMAIN

View File

@ -38,6 +38,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_call_later
import homeassistant.util.color as color_util
from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired,
@ -62,6 +63,7 @@ from . import (
DATA_DEVICE,
DATA_UPDATED,
DOMAIN,
POWER_STATE_CHANGE_TIME,
YEELIGHT_FLOW_TRANSITION_SCHEMA,
YeelightEntity,
)
@ -247,7 +249,7 @@ def _async_cmd(func):
except BULB_NETWORK_EXCEPTIONS as ex:
# A network error happened, the bulb is likely offline now
self.device.async_mark_unavailable()
self.async_write_ha_state()
self.async_state_changed()
exc_message = str(ex) or type(ex)
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
@ -419,13 +421,22 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
else:
self._custom_effects = {}
self._unexpected_state_check = None
@callback
def async_state_changed(self):
"""Call when the device changes state."""
if not self._device.available:
self._async_cancel_pending_state_check()
self.async_write_ha_state()
async def async_added_to_hass(self):
"""Handle entity which will be added."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
DATA_UPDATED.format(self._device.host),
self.async_write_ha_state,
self.async_state_changed,
)
)
await super().async_added_to_hass()
@ -760,6 +771,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
await self.async_set_default()
self._async_schedule_state_check(True)
@callback
def _async_cancel_pending_state_check(self):
"""Cancel a pending state check."""
if self._unexpected_state_check:
self._unexpected_state_check()
self._unexpected_state_check = None
@callback
def _async_schedule_state_check(self, expected_power_state):
"""Schedule a poll if the change failed to get pushed back to us.
Some devices (mainly nightlights) will not send back the on state
so we need to force a refresh.
"""
self._async_cancel_pending_state_check()
async def _async_update_if_state_unexpected(*_):
self._unexpected_state_check = None
if self.is_on != expected_power_state:
await self.device.async_update(True)
self._unexpected_state_check = async_call_later(
self.hass, POWER_STATE_CHANGE_TIME, _async_update_if_state_unexpected
)
@_async_cmd
async def _async_turn_off(self, duration) -> None:
"""Turn off with a given transition duration wrapped with _async_cmd."""
@ -775,6 +813,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
await self._async_turn_off(duration)
self._async_schedule_state_check(False)
@_async_cmd
async def async_set_mode(self, mode: str):

View File

@ -2,7 +2,7 @@
"domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
"requirements": ["zeroconf==0.36.7"],
"requirements": ["zeroconf==0.36.8"],
"dependencies": ["network", "api"],
"codeowners": ["@bdraco"],
"quality_scale": "internal",

View File

@ -5,7 +5,7 @@ from typing import Final
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 10
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@ -15,7 +15,7 @@ ciso8601==2.2.0
cryptography==3.4.8
emoji==1.5.0
hass-nabucasa==0.50.0
home-assistant-frontend==20211007.0
home-assistant-frontend==20211007.1
httpx==0.19.0
ifaddr==0.1.7
jinja2==3.0.1
@ -32,7 +32,7 @@ sqlalchemy==1.4.23
voluptuous-serialize==2.4.0
voluptuous==0.12.2
yarl==1.6.3
zeroconf==0.36.7
zeroconf==0.36.8
pycryptodome>=3.6.6

View File

@ -49,7 +49,7 @@ PyRMVtransport==0.3.2
PySocks==1.7.1
# homeassistant.components.switchbot
# PySwitchbot==0.11.0
# PySwitchbot==0.12.0
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@ -810,7 +810,7 @@ hole==0.5.1
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211007.0
home-assistant-frontend==20211007.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -1514,7 +1514,7 @@ pygtfs==0.1.6
pygti==0.9.2
# homeassistant.components.version
pyhaversion==21.7.0
pyhaversion==21.10.0
# homeassistant.components.heos
pyheos==0.7.2
@ -1670,7 +1670,7 @@ pynuki==1.4.1
pynut2==2.1.2
# homeassistant.components.nws
pynws==1.3.1
pynws==1.3.2
# homeassistant.components.nx584
pynx584==0.5
@ -2474,7 +2474,7 @@ youtube_dl==2021.04.26
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.36.7
zeroconf==0.36.8
# homeassistant.components.zha
zha-quirks==0.0.62

View File

@ -24,7 +24,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.2
# homeassistant.components.switchbot
# PySwitchbot==0.11.0
# PySwitchbot==0.12.0
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@ -485,7 +485,7 @@ hole==0.5.1
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211007.0
home-assistant-frontend==20211007.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -881,7 +881,7 @@ pygatt[GATTTOOL]==4.0.5
pygti==0.9.2
# homeassistant.components.version
pyhaversion==21.7.0
pyhaversion==21.10.0
# homeassistant.components.heos
pyheos==0.7.2
@ -986,7 +986,7 @@ pynuki==1.4.1
pynut2==2.1.2
# homeassistant.components.nws
pynws==1.3.1
pynws==1.3.2
# homeassistant.components.nx584
pynx584==0.5
@ -1409,7 +1409,7 @@ yeelight==0.7.7
youless-api==0.13
# homeassistant.components.zeroconf
zeroconf==0.36.7
zeroconf==0.36.8
# homeassistant.components.zha
zha-quirks==0.0.62

View File

@ -1,9 +1,15 @@
"""The tests for Efergy sensor platform."""
import asyncio
from datetime import timedelta
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import load_fixture
from tests.common import async_fire_time_changed, load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT"
@ -30,9 +36,14 @@ MULTI_SENSOR_CONFIG = {
}
def mock_responses(aioclient_mock: AiohttpClientMocker):
def mock_responses(aioclient_mock: AiohttpClientMocker, error: bool = False):
"""Mock responses for Efergy."""
base_url = "https://engage.efergy.com/mobile_proxy/"
if error:
aioclient_mock.get(
f"{base_url}getCurrentValuesSummary?token={token}", exc=asyncio.TimeoutError
)
return
aioclient_mock.get(
f"{base_url}getInstant?token={token}",
text=load_fixture("efergy/efergy_instant.json"),
@ -64,7 +75,9 @@ async def test_single_sensor_readings(
):
"""Test for successfully setting up the Efergy platform."""
mock_responses(aioclient_mock)
assert await async_setup_component(hass, "sensor", {"sensor": ONE_SENSOR_CONFIG})
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
await hass.async_block_till_done()
assert hass.states.get("sensor.energy_consumed").state == "38.21"
@ -79,9 +92,44 @@ async def test_multi_sensor_readings(
):
"""Test for multiple sensors in one household."""
mock_responses(aioclient_mock)
assert await async_setup_component(hass, "sensor", {"sensor": MULTI_SENSOR_CONFIG})
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MULTI_SENSOR_CONFIG}
)
await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == "218"
assert hass.states.get("sensor.efergy_0").state == "1808"
assert hass.states.get("sensor.efergy_728387").state == "312"
async def test_failed_getting_sids(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
):
"""Test failed gettings sids."""
mock_responses(aioclient_mock, error=True)
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
assert not hass.states.async_all("sensor")
async def test_failed_update_and_reconnection(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
):
"""Test failed update and reconnection."""
mock_responses(aioclient_mock)
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
aioclient_mock.clear_requests()
mock_responses(aioclient_mock, error=True)
next_update = dt_util.utcnow() + timedelta(seconds=3)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == STATE_UNAVAILABLE
aioclient_mock.clear_requests()
mock_responses(aioclient_mock)
next_update = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == "1628"

View File

@ -2,7 +2,7 @@
from unittest.mock import AsyncMock, MagicMock, patch
from kasa import SmartBulb, SmartPlug, SmartStrip
from kasa import SmartBulb, SmartDimmer, SmartPlug, SmartStrip
from kasa.exceptions import SmartDeviceException
from kasa.protocol import TPLinkSmartHomeProtocol
@ -48,6 +48,33 @@ def _mocked_bulb() -> SmartBulb:
return bulb
def _mocked_dimmer() -> SmartDimmer:
dimmer = MagicMock(auto_spec=SmartDimmer)
dimmer.update = AsyncMock()
dimmer.mac = MAC_ADDRESS
dimmer.alias = ALIAS
dimmer.model = MODEL
dimmer.host = IP_ADDRESS
dimmer.brightness = 50
dimmer.color_temp = 4000
dimmer.is_color = True
dimmer.is_strip = False
dimmer.is_plug = False
dimmer.is_dimmer = True
dimmer.hsv = (10, 30, 5)
dimmer.device_id = MAC_ADDRESS
dimmer.valid_temperature_range.min = 4000
dimmer.valid_temperature_range.max = 9000
dimmer.hw_info = {"sw_ver": "1.0.0"}
dimmer.turn_off = AsyncMock()
dimmer.turn_on = AsyncMock()
dimmer.set_brightness = AsyncMock()
dimmer.set_hsv = AsyncMock()
dimmer.set_color_temp = AsyncMock()
dimmer.protocol = _mock_protocol()
return dimmer
def _mocked_plug() -> SmartPlug:
plug = MagicMock(auto_spec=SmartPlug)
plug.update = AsyncMock()

View File

@ -4,14 +4,23 @@ from __future__ import annotations
from datetime import timedelta
from unittest.mock import MagicMock, patch
from homeassistant import setup
from homeassistant.components import tplink
from homeassistant.components.tplink.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from . import IP_ADDRESS, MAC_ADDRESS, _patch_discovery, _patch_single_discovery
from . import (
IP_ADDRESS,
MAC_ADDRESS,
_mocked_dimmer,
_patch_discovery,
_patch_single_discovery,
)
from tests.common import MockConfigEntry, async_fire_time_changed
@ -63,3 +72,73 @@ async def test_config_entry_retry(hass):
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done()
assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY
async def test_dimmer_switch_unique_id_fix_original_entity_was_deleted(
hass: HomeAssistant, entity_reg: EntityRegistry
):
"""Test that roll out unique id entity id changed to the original unique id."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS)
config_entry.add_to_hass(hass)
dimmer = _mocked_dimmer()
rollout_unique_id = MAC_ADDRESS.replace(":", "").upper()
original_unique_id = tplink.legacy_device_id(dimmer)
rollout_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=rollout_unique_id,
original_name="Rollout dimmer",
)
with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer):
await setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
migrated_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=original_unique_id,
original_name="Migrated dimmer",
)
assert migrated_dimmer_entity_reg.entity_id == rollout_dimmer_entity_reg.entity_id
async def test_dimmer_switch_unique_id_fix_original_entity_still_exists(
hass: HomeAssistant, entity_reg: EntityRegistry
):
"""Test no migration happens if the original entity id still exists."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS)
config_entry.add_to_hass(hass)
dimmer = _mocked_dimmer()
rollout_unique_id = MAC_ADDRESS.replace(":", "").upper()
original_unique_id = tplink.legacy_device_id(dimmer)
original_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=original_unique_id,
original_name="Original dimmer",
)
rollout_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=rollout_unique_id,
original_name="Rollout dimmer",
)
with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer):
await setup.async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
migrated_dimmer_entity_reg = entity_reg.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="light",
unique_id=original_unique_id,
original_name="Migrated dimmer",
)
assert migrated_dimmer_entity_reg.entity_id == original_dimmer_entity_reg.entity_id
assert migrated_dimmer_entity_reg.entity_id != rollout_dimmer_entity_reg.entity_id

View File

@ -1,5 +1,6 @@
"""Test the Yeelight light."""
import asyncio
from datetime import timedelta
import logging
import socket
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
@ -98,6 +99,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util.color import (
color_hs_to_RGB,
color_hs_to_xy,
@ -121,7 +123,7 @@ from . import (
_patch_discovery_interval,
)
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
CONFIG_ENTRY_DATA = {
CONF_HOST: IP_ADDRESS,
@ -1377,3 +1379,73 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant):
assert state.state == "on"
# bg_power off should not set the brightness to 0
assert state.attributes[ATTR_BRIGHTNESS] == 128
async def test_state_fails_to_update_triggers_update(hass: HomeAssistant):
"""Ensure we call async_get_properties if the turn on/off fails to update the state."""
mocked_bulb = _mocked_bulb()
properties = {**PROPERTIES}
properties.pop("active_mode")
properties["color_mode"] = "3" # HSV
mocked_bulb.last_properties = properties
mocked_bulb.bulb_type = BulbType.Color
config_entry = MockConfigEntry(
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
)
config_entry.add_to_hass(hass)
with _patch_discovery(), _patch_discovery_interval(), patch(
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# We use asyncio.create_task now to avoid
# blocking starting so we need to block again
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 1
mocked_bulb.last_properties["power"] = "off"
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
mocked_bulb.last_properties["power"] = "on"
for _ in range(5):
await hass.services.async_call(
"light",
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_off.mock_calls) == 5
# Even with five calls we only do one state request
# since each successive call should cancel the unexpected
# state check
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 3
# But if the state is correct no calls
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 3