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

View File

@ -1,7 +1,9 @@
"""Support for Efergy sensors.""" """Support for Efergy sensors."""
from __future__ import annotations from __future__ import annotations
from pyefergy import Efergy import logging
from pyefergy import Efergy, exceptions
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -20,6 +22,7 @@ from homeassistant.const import (
POWER_WATT, POWER_WATT,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -39,6 +42,8 @@ CONF_CURRENT_VALUES = "current_values"
DEFAULT_PERIOD = "year" DEFAULT_PERIOD = "year"
DEFAULT_UTC_OFFSET = "0" DEFAULT_UTC_OFFSET = "0"
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES: dict[str, SensorEntityDescription] = { SENSOR_TYPES: dict[str, SensorEntityDescription] = {
CONF_INSTANT: SensorEntityDescription( CONF_INSTANT: SensorEntityDescription(
key=CONF_INSTANT, key=CONF_INSTANT,
@ -102,7 +107,10 @@ async def async_setup_platform(
) )
dev = [] dev = []
sensors = await api.get_sids() 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]: for variable in config[CONF_MONITORED_VARIABLES]:
if variable[CONF_TYPE] == CONF_CURRENT_VALUES: if variable[CONF_TYPE] == CONF_CURRENT_VALUES:
for sensor in sensors: for sensor in sensors:
@ -150,6 +158,15 @@ class EfergySensor(SensorEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Get the Efergy monitor data from the web service.""" """Get the Efergy monitor data from the web service."""
self._attr_native_value = await self.api.async_get_reading( try:
self.entity_description.key, period=self.period, sid=self.sid 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", "name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [ "requirements": [
"home-assistant-frontend==20211007.0" "home-assistant-frontend==20211007.1"
], ],
"dependencies": [ "dependencies": [
"api", "api",

View File

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

View File

@ -35,7 +35,7 @@ DEVICE_ICONS = {
0: "mdi:access-point-network", # Router (Orbi ...) 0: "mdi:access-point-network", # Router (Orbi ...)
1: "mdi:book-open-variant", # Amazon Kindle 1: "mdi:book-open-variant", # Amazon Kindle
2: "mdi:android", # Android Device 2: "mdi:android", # Android Device
3: "mdi:cellphone-android", # Android Phone 3: "mdi:cellphone", # Android Phone
4: "mdi:tablet-android", # Android Tablet 4: "mdi:tablet-android", # Android Tablet
5: "mdi:router-wireless", # Apple Airport Express 5: "mdi:router-wireless", # Apple Airport Express
6: "mdi:disc-player", # Blu-ray Player 6: "mdi:disc-player", # Blu-ray Player
@ -46,15 +46,15 @@ DEVICE_ICONS = {
11: "mdi:play-network", # DVR 11: "mdi:play-network", # DVR
12: "mdi:gamepad-variant", # Gaming Console 12: "mdi:gamepad-variant", # Gaming Console
13: "mdi:desktop-mac", # iMac 13: "mdi:desktop-mac", # iMac
14: "mdi:tablet-ipad", # iPad 14: "mdi:tablet", # iPad
15: "mdi:tablet-ipad", # iPad Mini 15: "mdi:tablet", # iPad Mini
16: "mdi:cellphone-iphone", # iPhone 5/5S/5C 16: "mdi:cellphone", # iPhone 5/5S/5C
17: "mdi:cellphone-iphone", # iPhone 17: "mdi:cellphone", # iPhone
18: "mdi:ipod", # iPod Touch 18: "mdi:ipod", # iPod Touch
19: "mdi:linux", # Linux PC 19: "mdi:linux", # Linux PC
20: "mdi:apple-finder", # Mac Mini 20: "mdi:apple-finder", # Mac Mini
21: "mdi:desktop-tower", # Mac Pro 21: "mdi:desktop-tower", # Mac Pro
22: "mdi:laptop-mac", # MacBook 22: "mdi:laptop", # MacBook
23: "mdi:play-network", # Media Device 23: "mdi:play-network", # Media Device
24: "mdi:network", # Network Device 24: "mdi:network", # Network Device
25: "mdi:play-network", # Other STB 25: "mdi:play-network", # Other STB
@ -71,7 +71,7 @@ DEVICE_ICONS = {
36: "mdi:tablet", # Tablet 36: "mdi:tablet", # Tablet
37: "mdi:desktop-classic", # UNIX PC 37: "mdi:desktop-classic", # UNIX PC
38: "mdi:desktop-tower-monitor", # Windows PC 38: "mdi:desktop-tower-monitor", # Windows PC
39: "mdi:laptop-windows", # Surface 39: "mdi:laptop", # Surface
40: "mdi:access-point-network", # Wifi Extender 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)", "name": "National Weather Service (NWS)",
"documentation": "https://www.home-assistant.io/integrations/nws", "documentation": "https://www.home-assistant.io/integrations/nws",
"codeowners": ["@MatthewFlamm"], "codeowners": ["@MatthewFlamm"],
"requirements": ["pynws==1.3.1"], "requirements": ["pynws==1.3.2"],
"quality_scale": "platinum", "quality_scale": "platinum",
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"

View File

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

View File

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

View File

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

View File

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

View File

@ -46,6 +46,7 @@ from .const import (
LIGHT_TRANSITION_MIN_FIRMWARE_DATE, LIGHT_TRANSITION_MIN_FIRMWARE_DATE,
MAX_TRANSITION_TIME, MAX_TRANSITION_TIME,
MODELS_SUPPORTING_LIGHT_TRANSITION, MODELS_SUPPORTING_LIGHT_TRANSITION,
RGBW_MODELS,
RPC, RPC,
SHBLB_1_RGB_EFFECTS, SHBLB_1_RGB_EFFECTS,
STANDARD_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"): if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"):
self._min_kelvin = KELVIN_MIN_VALUE_COLOR 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) self._supported_color_modes.add(COLOR_MODE_RGBW)
else: else:
self._supported_color_modes.add(COLOR_MODE_RGB) 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: if settings["device"]["type"] in SHBTN_MODELS:
return True 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") button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
if button is None: if button is None:
return False return False

View File

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

View File

@ -11,6 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import network from homeassistant.components import network
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -19,7 +20,11 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
) )
from homeassistant.core import HomeAssistant, callback 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.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -158,12 +163,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except SmartDeviceException as ex: except SmartDeviceException as ex:
raise ConfigEntryNotReady from 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.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device)
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True 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: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
hass_data: dict[str, Any] = hass.data[DOMAIN] 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, color_temperature_mired_to_kelvin as mired_to_kelvin,
) )
from . import legacy_device_id
from .const import DOMAIN from .const import DOMAIN
from .coordinator import TPLinkDataUpdateCoordinator from .coordinator import TPLinkDataUpdateCoordinator
from .entity import CoordinatedTPLinkEntity, async_refresh_after from .entity import CoordinatedTPLinkEntity, async_refresh_after
@ -58,7 +59,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
"""Initialize the switch.""" """Initialize the switch."""
super().__init__(device, coordinator) super().__init__(device, coordinator)
# For backwards compat with pyHS100 # For backwards compat with pyHS100
self._attr_unique_id = self.device.mac.replace(":", "").upper() 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 @async_refresh_after
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:

View File

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

View File

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

View File

@ -38,6 +38,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_call_later
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.util.color import ( from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired, color_temperature_kelvin_to_mired as kelvin_to_mired,
@ -62,6 +63,7 @@ from . import (
DATA_DEVICE, DATA_DEVICE,
DATA_UPDATED, DATA_UPDATED,
DOMAIN, DOMAIN,
POWER_STATE_CHANGE_TIME,
YEELIGHT_FLOW_TRANSITION_SCHEMA, YEELIGHT_FLOW_TRANSITION_SCHEMA,
YeelightEntity, YeelightEntity,
) )
@ -247,7 +249,7 @@ def _async_cmd(func):
except BULB_NETWORK_EXCEPTIONS as ex: except BULB_NETWORK_EXCEPTIONS as ex:
# A network error happened, the bulb is likely offline now # A network error happened, the bulb is likely offline now
self.device.async_mark_unavailable() self.device.async_mark_unavailable()
self.async_write_ha_state() self.async_state_changed()
exc_message = str(ex) or type(ex) exc_message = str(ex) or type(ex)
raise HomeAssistantError( raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" 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: else:
self._custom_effects = {} 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): async def async_added_to_hass(self):
"""Handle entity which will be added.""" """Handle entity which will be added."""
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, self.hass,
DATA_UPDATED.format(self._device.host), DATA_UPDATED.format(self._device.host),
self.async_write_ha_state, self.async_state_changed,
) )
) )
await super().async_added_to_hass() 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): if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
await self.async_set_default() 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_cmd
async def _async_turn_off(self, duration) -> None: async def _async_turn_off(self, duration) -> None:
"""Turn off with a given transition duration wrapped with _async_cmd.""" """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 duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
await self._async_turn_off(duration) await self._async_turn_off(duration)
self._async_schedule_state_check(False)
@_async_cmd @_async_cmd
async def async_set_mode(self, mode: str): async def async_set_mode(self, mode: str):

View File

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

View File

@ -5,7 +5,7 @@ from typing import Final
MAJOR_VERSION: Final = 2021 MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 10 MINOR_VERSION: Final = 10
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) 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 cryptography==3.4.8
emoji==1.5.0 emoji==1.5.0
hass-nabucasa==0.50.0 hass-nabucasa==0.50.0
home-assistant-frontend==20211007.0 home-assistant-frontend==20211007.1
httpx==0.19.0 httpx==0.19.0
ifaddr==0.1.7 ifaddr==0.1.7
jinja2==3.0.1 jinja2==3.0.1
@ -32,7 +32,7 @@ sqlalchemy==1.4.23
voluptuous-serialize==2.4.0 voluptuous-serialize==2.4.0
voluptuous==0.12.2 voluptuous==0.12.2
yarl==1.6.3 yarl==1.6.3
zeroconf==0.36.7 zeroconf==0.36.8
pycryptodome>=3.6.6 pycryptodome>=3.6.6

View File

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

View File

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

View File

@ -1,9 +1,15 @@
"""The tests for Efergy sensor platform.""" """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.core import HomeAssistant
from homeassistant.setup import async_setup_component 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 from tests.test_util.aiohttp import AiohttpClientMocker
token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT" 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.""" """Mock responses for Efergy."""
base_url = "https://engage.efergy.com/mobile_proxy/" 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( aioclient_mock.get(
f"{base_url}getInstant?token={token}", f"{base_url}getInstant?token={token}",
text=load_fixture("efergy/efergy_instant.json"), 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.""" """Test for successfully setting up the Efergy platform."""
mock_responses(aioclient_mock) 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() await hass.async_block_till_done()
assert hass.states.get("sensor.energy_consumed").state == "38.21" 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.""" """Test for multiple sensors in one household."""
mock_responses(aioclient_mock) 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() await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == "218" assert hass.states.get("sensor.efergy_728386").state == "218"
assert hass.states.get("sensor.efergy_0").state == "1808" assert hass.states.get("sensor.efergy_0").state == "1808"
assert hass.states.get("sensor.efergy_728387").state == "312" 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 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.exceptions import SmartDeviceException
from kasa.protocol import TPLinkSmartHomeProtocol from kasa.protocol import TPLinkSmartHomeProtocol
@ -48,6 +48,33 @@ def _mocked_bulb() -> SmartBulb:
return bulb 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: def _mocked_plug() -> SmartPlug:
plug = MagicMock(auto_spec=SmartPlug) plug = MagicMock(auto_spec=SmartPlug)
plug.update = AsyncMock() plug.update = AsyncMock()

View File

@ -4,14 +4,23 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from homeassistant import setup
from homeassistant.components import tplink from homeassistant.components import tplink
from homeassistant.components.tplink.const import DOMAIN from homeassistant.components.tplink.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED 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.setup import async_setup_component
from homeassistant.util import dt as dt_util 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 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 async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done() await hass.async_block_till_done()
assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY 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.""" """Test the Yeelight light."""
import asyncio import asyncio
from datetime import timedelta
import logging import logging
import socket import socket
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch 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.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util.color import ( from homeassistant.util.color import (
color_hs_to_RGB, color_hs_to_RGB,
color_hs_to_xy, color_hs_to_xy,
@ -121,7 +123,7 @@ from . import (
_patch_discovery_interval, _patch_discovery_interval,
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_fire_time_changed
CONFIG_ENTRY_DATA = { CONFIG_ENTRY_DATA = {
CONF_HOST: IP_ADDRESS, CONF_HOST: IP_ADDRESS,
@ -1377,3 +1379,73 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant):
assert state.state == "on" assert state.state == "on"
# bg_power off should not set the brightness to 0 # bg_power off should not set the brightness to 0
assert state.attributes[ATTR_BRIGHTNESS] == 128 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