mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Merge pull request #57455 from home-assistant/rc
This commit is contained in:
commit
a3ae25efdb
@ -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",
|
||||||
|
@ -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 = []
|
||||||
|
try:
|
||||||
sensors = await api.get_sids()
|
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."""
|
||||||
|
try:
|
||||||
self._attr_native_value = await self.api.async_get_reading(
|
self._attr_native_value = await self.api.async_get_reading(
|
||||||
self.entity_description.key, period=self.period, sid=self.sid
|
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")
|
||||||
|
@ -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",
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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(
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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]
|
||||||
|
@ -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,6 +59,13 @@ 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
|
||||||
|
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()
|
self._attr_unique_id = self.device.mac.replace(":", "").upper()
|
||||||
|
|
||||||
@async_refresh_after
|
@async_refresh_after
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user