mirror of
https://github.com/home-assistant/core.git
synced 2026-05-03 19:41:15 +02:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3ae25efdb | |||
| ce75c11033 | |||
| 862527782e | |||
| b102a45bdf | |||
| fc5a2e3e06 | |||
| c01549999a | |||
| 7ed50bb9e6 | |||
| 975b48859e | |||
| e92adde417 | |||
| d929faaeff | |||
| 8c44f9b501 | |||
| 2e9bbe5219 | |||
| a4b6b74997 | |||
| 307a07611e | |||
| 0f2b9a6d77 | |||
| 1e48bef74e | |||
| 438bdcbd8b | |||
| 67e5f4e823 | |||
| b110a5bbfc | |||
| 3c2f3f2ade | |||
| 1ea0891602 | |||
| 54acc5bade | |||
| 0c886e2990 | |||
| f10a469d4f | |||
| 0358a13536 | |||
| 1a78c461a0 | |||
| c6d506cb4f | |||
| 5bb4bc3d13 | |||
| 387249b593 | |||
| 1a376b25af | |||
| ed4b44c126 | |||
| ef7f1ffddc | |||
| e3e64130f1 | |||
| 4e49e3a3fb | |||
| fd371f2887 | |||
| ec0256e27f | |||
| 425015eb8b | |||
| 0b26b15749 | |||
| 06befe906b | |||
| bf4a3d8d35 | |||
| 691d8d6b80 | |||
| 7f49e02a4d | |||
| 7544ec2399 | |||
| 32889dbfbe | |||
| 46394b50d8 | |||
| 25fc479cd4 | |||
| 9636799dfb | |||
| e01e575092 | |||
| 2fc9cdbe68 | |||
| 72b3bc13e4 | |||
| 9e755fcc49 |
@@ -187,16 +187,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="not_bosch_shc")
|
||||
|
||||
try:
|
||||
self.info = info = await self._get_info(discovery_info["host"])
|
||||
hosts = (
|
||||
discovery_info["host"]
|
||||
if isinstance(discovery_info["host"], list)
|
||||
else [discovery_info["host"]]
|
||||
)
|
||||
for host in hosts:
|
||||
if host.startswith("169."): # skip link local address
|
||||
continue
|
||||
self.info = await self._get_info(host)
|
||||
self.host = host
|
||||
if self.host is None:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except SHCConnectionError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
local_name = discovery_info["hostname"][:-1]
|
||||
node_name = local_name[: -len(".local")]
|
||||
|
||||
await self.async_set_unique_id(info["unique_id"])
|
||||
self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]})
|
||||
self.host = discovery_info["host"]
|
||||
await self.async_set_unique_id(self.info["unique_id"])
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
||||
self.context["title_placeholders"] = {"name": node_name}
|
||||
return await self.async_step_confirm_discovery()
|
||||
|
||||
|
||||
@@ -75,7 +75,8 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
uuid=uuid,
|
||||
password=password,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
self.host = None
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.schema,
|
||||
@@ -87,13 +88,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
data_schema=self.schema,
|
||||
errors={"base": "invalid_auth"},
|
||||
)
|
||||
except ClientError:
|
||||
_LOGGER.exception("ClientError")
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.schema,
|
||||
errors={"base": "unknown"},
|
||||
)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected error creating device")
|
||||
return self.async_show_form(
|
||||
@@ -109,6 +103,13 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""User initiated config flow."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user", data_schema=self.schema)
|
||||
if user_input.get(CONF_API_KEY) and user_input.get(CONF_PASSWORD):
|
||||
self.host = user_input.get(CONF_HOST)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.schema,
|
||||
errors={"base": "api_password"},
|
||||
)
|
||||
return await self._create_device(
|
||||
user_input[CONF_HOST],
|
||||
user_input.get(CONF_API_KEY),
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"error": {
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"api_password": "[%key:common::config_flow::error::invalid_auth%], use either API Key or Password.",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"cannot_connect": "Failed to connect"
|
||||
},
|
||||
"error": {
|
||||
"api_password": "Invalid authentication, use either API Key or Password.",
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "DLNA Digital Media Renderer",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"requirements": ["async-upnp-client==0.22.5"],
|
||||
"requirements": ["async-upnp-client==0.22.8"],
|
||||
"dependencies": ["network", "ssdp"],
|
||||
"codeowners": ["@StevenLooman", "@chishm"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -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 = []
|
||||
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]:
|
||||
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."""
|
||||
self._attr_native_value = await self.api.async_get_reading(
|
||||
self.entity_description.key, period=self.period, sid=self.sid
|
||||
)
|
||||
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")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20211004.0"
|
||||
"home-assistant-frontend==20211007.1"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
||||
@@ -259,25 +259,31 @@ class CoverGroup(GroupEntity, CoverEntity):
|
||||
"""Update state and attributes."""
|
||||
self._attr_assumed_state = False
|
||||
|
||||
self._attr_is_closed = None
|
||||
self._attr_is_closed = True
|
||||
self._attr_is_closing = False
|
||||
self._attr_is_opening = False
|
||||
has_valid_state = False
|
||||
for entity_id in self._entities:
|
||||
state = self.hass.states.get(entity_id)
|
||||
if not state:
|
||||
continue
|
||||
if state.state == STATE_OPEN:
|
||||
self._attr_is_closed = False
|
||||
has_valid_state = True
|
||||
continue
|
||||
if state.state == STATE_CLOSED:
|
||||
self._attr_is_closed = True
|
||||
has_valid_state = True
|
||||
continue
|
||||
if state.state == STATE_CLOSING:
|
||||
self._attr_is_closing = True
|
||||
has_valid_state = True
|
||||
continue
|
||||
if state.state == STATE_OPENING:
|
||||
self._attr_is_opening = True
|
||||
has_valid_state = True
|
||||
continue
|
||||
if not has_valid_state:
|
||||
self._attr_is_closed = None
|
||||
|
||||
position_covers = self._covers[KEY_POSITION]
|
||||
all_position_states = [self.hass.states.get(x) for x in position_covers]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "HomeKit",
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit",
|
||||
"requirements": [
|
||||
"HAP-python==4.2.1",
|
||||
"HAP-python==4.3.0",
|
||||
"fnvhash==0.1.0",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
|
||||
@@ -81,6 +81,30 @@ def has_date_or_time(conf):
|
||||
raise vol.Invalid("Entity needs at least a date or a time")
|
||||
|
||||
|
||||
def valid_initial(conf):
|
||||
"""Check the initial value is valid."""
|
||||
initial = conf.get(CONF_INITIAL)
|
||||
if not initial:
|
||||
return conf
|
||||
|
||||
if conf[CONF_HAS_DATE] and conf[CONF_HAS_TIME]:
|
||||
parsed_value = dt_util.parse_datetime(initial)
|
||||
if parsed_value is not None:
|
||||
return conf
|
||||
raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a datetime")
|
||||
|
||||
if conf[CONF_HAS_DATE]:
|
||||
parsed_value = dt_util.parse_date(initial)
|
||||
if parsed_value is not None:
|
||||
return conf
|
||||
raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a date")
|
||||
|
||||
parsed_value = dt_util.parse_time(initial)
|
||||
if parsed_value is not None:
|
||||
return conf
|
||||
raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a time")
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: cv.schema_with_slug_keys(
|
||||
@@ -93,6 +117,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
},
|
||||
has_date_or_time,
|
||||
valid_initial,
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ from iotawattpy.sensor import Sensor
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
STATE_CLASS_TOTAL,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
@@ -83,6 +83,7 @@ ENTITY_DESCRIPTION_KEY_MAP: dict[str, IotaWattSensorEntityDescription] = {
|
||||
"WattHours": IotaWattSensorEntityDescription(
|
||||
"WattHours",
|
||||
native_unit_of_measurement=ENERGY_WATT_HOUR,
|
||||
state_class=STATE_CLASS_TOTAL,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
),
|
||||
"VA": IotaWattSensorEntityDescription(
|
||||
@@ -242,7 +243,6 @@ class IotaWattAccumulatingSensor(IotaWattSensor, RestoreEntity):
|
||||
|
||||
super().__init__(coordinator, key, entity_description)
|
||||
|
||||
self._attr_state_class = STATE_CLASS_TOTAL_INCREASING
|
||||
if self._attr_unique_id is not None:
|
||||
self._attr_unique_id += ".accumulated"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "mill",
|
||||
"name": "Mill",
|
||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||
"requirements": ["millheater==0.6.0"],
|
||||
"requirements": ["millheater==0.6.1"],
|
||||
"codeowners": ["@danielhiversen"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
|
||||
@@ -168,8 +168,8 @@ class NanoleafLight(LightEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Instruct the light to turn off."""
|
||||
transition = kwargs.get(ATTR_TRANSITION)
|
||||
await self._nanoleaf.turn_off(transition)
|
||||
transition: float | None = kwargs.get(ATTR_TRANSITION)
|
||||
await self._nanoleaf.turn_off(None if transition is None else int(transition))
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Fetch new state data for this light."""
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Nanoleaf",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/nanoleaf",
|
||||
"requirements": ["aionanoleaf==0.0.2"],
|
||||
"requirements": ["aionanoleaf==0.0.3"],
|
||||
"zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."],
|
||||
"homekit" : {
|
||||
"models": [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Config flow to configure the Netgear integration."""
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pynetgear import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USER
|
||||
@@ -20,6 +21,8 @@ from .const import CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, DEFAULT_NAME, DOMA
|
||||
from .errors import CannotLoginException
|
||||
from .router import get_api
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _discovery_schema_with_defaults(discovery_info):
|
||||
return vol.Schema(_ordered_shared_schema(discovery_info))
|
||||
@@ -120,15 +123,19 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
device_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
|
||||
if device_url.hostname:
|
||||
updated_data[CONF_HOST] = device_url.hostname
|
||||
if device_url.port:
|
||||
updated_data[CONF_PORT] = device_url.port
|
||||
if device_url.scheme == "https":
|
||||
updated_data[CONF_SSL] = True
|
||||
else:
|
||||
updated_data[CONF_SSL] = False
|
||||
|
||||
_LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info)
|
||||
|
||||
await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_SERIAL])
|
||||
self._abort_if_unique_id_configured(updates=updated_data)
|
||||
|
||||
if device_url.port:
|
||||
updated_data[CONF_PORT] = device_url.port
|
||||
|
||||
self.placeholders.update(updated_data)
|
||||
self.discovered = True
|
||||
|
||||
|
||||
@@ -11,14 +11,31 @@ DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
|
||||
DEFAULT_NAME = "Netgear router"
|
||||
|
||||
# update method V2 models
|
||||
MODELS_V2 = ["Orbi"]
|
||||
MODELS_V2 = [
|
||||
"Orbi",
|
||||
"RBK",
|
||||
"RBR",
|
||||
"RBS",
|
||||
"RBW",
|
||||
"LBK",
|
||||
"LBR",
|
||||
"CBK",
|
||||
"CBR",
|
||||
"SRC",
|
||||
"SRK",
|
||||
"SRR",
|
||||
"SRS",
|
||||
"SXK",
|
||||
"SXR",
|
||||
"SXS",
|
||||
]
|
||||
|
||||
# Icons
|
||||
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
|
||||
@@ -29,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
|
||||
@@ -54,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
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
||||
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
|
||||
SOURCE_TYPE_ROUTER,
|
||||
)
|
||||
@@ -50,7 +51,7 @@ async def async_get_scanner(hass, config):
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
data=config[DEVICE_TRACKER_DOMAIN],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -146,8 +146,9 @@ class NetgearRouter:
|
||||
self.model = self._info.get("ModelName")
|
||||
self.firmware_version = self._info.get("Firmwareversion")
|
||||
|
||||
if self.model in MODELS_V2:
|
||||
self._method_version = 2
|
||||
for model in MODELS_V2:
|
||||
if self.model.startswith(model):
|
||||
self._method_version = 2
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up a Netgear router."""
|
||||
@@ -198,12 +199,12 @@ class NetgearRouter:
|
||||
ntg_devices = await self.async_get_attached_devices()
|
||||
now = dt_util.utcnow()
|
||||
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_LOGGER.debug("Netgear scan result: \n%s", ntg_devices)
|
||||
|
||||
for ntg_device in ntg_devices:
|
||||
device_mac = format_mac(ntg_device.mac)
|
||||
|
||||
if self._method_version == 2 and not ntg_device.link_rate:
|
||||
continue
|
||||
|
||||
if not self.devices.get(device_mac):
|
||||
new_device = True
|
||||
|
||||
@@ -273,8 +274,8 @@ class NetgearDeviceEntity(Entity):
|
||||
"""Return the device information."""
|
||||
return {
|
||||
"connections": {(CONNECTION_NETWORK_MAC, self._mac)},
|
||||
"name": self._device_name,
|
||||
"model": self._device["device_model"],
|
||||
"default_name": self._device_name,
|
||||
"default_model": self._device["device_model"],
|
||||
"via_device": (DOMAIN, self._router.unique_id),
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -91,11 +91,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry_id = entry.entry_id
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN].setdefault(entry_id, {})
|
||||
http_session = requests.Session()
|
||||
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||
|
||||
password = entry.data.get(CONF_PASSWORD)
|
||||
power_wall = Powerwall(entry.data[CONF_IP_ADDRESS], http_session=http_session)
|
||||
power_wall = Powerwall(ip_address, http_session=http_session)
|
||||
try:
|
||||
powerwall_data = await hass.async_add_executor_job(
|
||||
_login_and_fetch_base_info, power_wall, password
|
||||
@@ -115,13 +115,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await _migrate_old_unique_ids(hass, entry_id, powerwall_data)
|
||||
login_failed_count = 0
|
||||
|
||||
runtime_data = hass.data[DOMAIN][entry.entry_id] = {
|
||||
POWERWALL_API_CHANGED: False,
|
||||
POWERWALL_HTTP_SESSION: http_session,
|
||||
}
|
||||
|
||||
def _recreate_powerwall_login():
|
||||
nonlocal http_session
|
||||
nonlocal power_wall
|
||||
http_session.close()
|
||||
http_session = requests.Session()
|
||||
power_wall = Powerwall(ip_address, http_session=http_session)
|
||||
runtime_data[POWERWALL_OBJECT] = power_wall
|
||||
runtime_data[POWERWALL_HTTP_SESSION] = http_session
|
||||
power_wall.login("", password)
|
||||
|
||||
async def async_update_data():
|
||||
"""Fetch data from API endpoint."""
|
||||
# Check if we had an error before
|
||||
nonlocal login_failed_count
|
||||
_LOGGER.debug("Checking if update failed")
|
||||
if hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]:
|
||||
return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data
|
||||
if runtime_data[POWERWALL_API_CHANGED]:
|
||||
return runtime_data[POWERWALL_COORDINATOR].data
|
||||
|
||||
_LOGGER.debug("Updating data")
|
||||
try:
|
||||
@@ -130,9 +145,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if password is None:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
# If the session expired, relogin, and try again
|
||||
# If the session expired, recreate, relogin, and try again
|
||||
try:
|
||||
await hass.async_add_executor_job(power_wall.login, "", password)
|
||||
await hass.async_add_executor_job(_recreate_powerwall_login)
|
||||
return await _async_update_powerwall_data(hass, entry, power_wall)
|
||||
except AccessDeniedError as ex:
|
||||
login_failed_count += 1
|
||||
@@ -153,13 +168,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = powerwall_data
|
||||
hass.data[DOMAIN][entry.entry_id].update(
|
||||
runtime_data.update(
|
||||
{
|
||||
**powerwall_data,
|
||||
POWERWALL_OBJECT: power_wall,
|
||||
POWERWALL_COORDINATOR: coordinator,
|
||||
POWERWALL_HTTP_SESSION: http_session,
|
||||
POWERWALL_API_CHANGED: False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -470,17 +470,20 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901
|
||||
elif new_version == 18:
|
||||
# Recreate the statistics and statistics meta tables.
|
||||
#
|
||||
# Order matters! Statistics has a relation with StatisticsMeta,
|
||||
# so statistics need to be deleted before meta (or in pair depending
|
||||
# on the SQL backend); and meta needs to be created before statistics.
|
||||
if sqlalchemy.inspect(engine).has_table(
|
||||
StatisticsMeta.__tablename__
|
||||
) or sqlalchemy.inspect(engine).has_table(Statistics.__tablename__):
|
||||
Base.metadata.drop_all(
|
||||
bind=engine, tables=[Statistics.__table__, StatisticsMeta.__table__]
|
||||
)
|
||||
# Order matters! Statistics and StatisticsShortTerm have a relation with
|
||||
# StatisticsMeta, so statistics need to be deleted before meta (or in pair
|
||||
# depending on the SQL backend); and meta needs to be created before statistics.
|
||||
Base.metadata.drop_all(
|
||||
bind=engine,
|
||||
tables=[
|
||||
StatisticsShortTerm.__table__,
|
||||
Statistics.__table__,
|
||||
StatisticsMeta.__table__,
|
||||
],
|
||||
)
|
||||
|
||||
StatisticsMeta.__table__.create(engine)
|
||||
StatisticsShortTerm.__table__.create(engine)
|
||||
Statistics.__table__.create(engine)
|
||||
elif new_version == 19:
|
||||
# This adds the statistic runs table, insert a fake run to prevent duplicating
|
||||
@@ -527,23 +530,15 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901
|
||||
# so statistics need to be deleted before meta (or in pair depending
|
||||
# on the SQL backend); and meta needs to be created before statistics.
|
||||
if engine.dialect.name == "oracle":
|
||||
if (
|
||||
sqlalchemy.inspect(engine).has_table(StatisticsMeta.__tablename__)
|
||||
or sqlalchemy.inspect(engine).has_table(Statistics.__tablename__)
|
||||
or sqlalchemy.inspect(engine).has_table(StatisticsRuns.__tablename__)
|
||||
or sqlalchemy.inspect(engine).has_table(
|
||||
StatisticsShortTerm.__tablename__
|
||||
)
|
||||
):
|
||||
Base.metadata.drop_all(
|
||||
bind=engine,
|
||||
tables=[
|
||||
StatisticsShortTerm.__table__,
|
||||
Statistics.__table__,
|
||||
StatisticsMeta.__table__,
|
||||
StatisticsRuns.__table__,
|
||||
],
|
||||
)
|
||||
Base.metadata.drop_all(
|
||||
bind=engine,
|
||||
tables=[
|
||||
StatisticsShortTerm.__table__,
|
||||
Statistics.__table__,
|
||||
StatisticsMeta.__table__,
|
||||
StatisticsRuns.__table__,
|
||||
],
|
||||
)
|
||||
|
||||
StatisticsRuns.__table__.create(engine)
|
||||
StatisticsMeta.__table__.create(engine)
|
||||
|
||||
@@ -13,6 +13,7 @@ from sqlalchemy import bindparam, func
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext import baked
|
||||
from sqlalchemy.orm.scoping import scoped_session
|
||||
from sqlalchemy.sql.expression import true
|
||||
|
||||
from homeassistant.const import (
|
||||
PRESSURE_PA,
|
||||
@@ -396,9 +397,9 @@ def get_metadata_with_session(
|
||||
StatisticsMeta.statistic_id.in_(bindparam("statistic_ids"))
|
||||
)
|
||||
if statistic_type == "mean":
|
||||
baked_query += lambda q: q.filter(StatisticsMeta.has_mean.isnot(False))
|
||||
baked_query += lambda q: q.filter(StatisticsMeta.has_mean == true())
|
||||
elif statistic_type == "sum":
|
||||
baked_query += lambda q: q.filter(StatisticsMeta.has_sum.isnot(False))
|
||||
baked_query += lambda q: q.filter(StatisticsMeta.has_sum == true())
|
||||
result = execute(baked_query(session).params(statistic_ids=statistic_ids))
|
||||
if not result:
|
||||
return {}
|
||||
|
||||
@@ -7,6 +7,7 @@ import datetime
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
@@ -362,13 +363,14 @@ def _wanted_statistics(sensor_states: list[State]) -> dict[str, set[str]]:
|
||||
return wanted_statistics
|
||||
|
||||
|
||||
def _last_reset_as_utc_isoformat(
|
||||
last_reset_s: str | None, entity_id: str
|
||||
) -> str | None:
|
||||
def _last_reset_as_utc_isoformat(last_reset_s: Any, entity_id: str) -> str | None:
|
||||
"""Parse last_reset and convert it to UTC."""
|
||||
if last_reset_s is None:
|
||||
return None
|
||||
last_reset = dt_util.parse_datetime(last_reset_s)
|
||||
if isinstance(last_reset_s, str):
|
||||
last_reset = dt_util.parse_datetime(last_reset_s)
|
||||
else:
|
||||
last_reset = None
|
||||
if last_reset is None:
|
||||
_LOGGER.warning(
|
||||
"Ignoring invalid last reset '%s' for %s", last_reset_s, entity_id
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "ssdp",
|
||||
"name": "Simple Service Discovery Protocol (SSDP)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ssdp",
|
||||
"requirements": ["async-upnp-client==0.22.5"],
|
||||
"requirements": ["async-upnp-client==0.22.8"],
|
||||
"dependencies": ["network"],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": [],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from kasa import SmartDevice, SmartDeviceException
|
||||
@@ -10,10 +11,21 @@ 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, CONF_MAC, CONF_NAME
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_NAME,
|
||||
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
|
||||
|
||||
from .const import (
|
||||
@@ -32,6 +44,8 @@ from .migration import (
|
||||
async_migrate_yaml_entries,
|
||||
)
|
||||
|
||||
DISCOVERY_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@@ -111,6 +125,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_migrate_legacy_entries(
|
||||
hass, hosts_by_mac, config_entries_by_mac, legacy_entry
|
||||
)
|
||||
# Migrate the yaml entry that was previously imported
|
||||
async_migrate_yaml_entries(hass, legacy_entry.data)
|
||||
|
||||
if conf is not None:
|
||||
async_migrate_yaml_entries(hass, conf)
|
||||
@@ -118,6 +134,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if discovered_devices:
|
||||
async_trigger_discovery(hass, discovered_devices)
|
||||
|
||||
async def _async_discovery(*_: Any) -> None:
|
||||
if discovered := await async_discover_devices(hass):
|
||||
async_trigger_discovery(hass, discovered)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_discovery)
|
||||
async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -140,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]
|
||||
|
||||
@@ -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,15 +59,32 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device, coordinator)
|
||||
# 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 def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
transition = kwargs.get(ATTR_TRANSITION)
|
||||
if (transition := kwargs.get(ATTR_TRANSITION)) is not None:
|
||||
transition = int(transition * 1_000)
|
||||
|
||||
if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None:
|
||||
brightness = round((brightness * 100.0) / 255.0)
|
||||
|
||||
if self.device.is_dimmer and transition is None:
|
||||
# This is a stopgap solution for inconsistent set_brightness handling
|
||||
# in the upstream library, see #57265.
|
||||
# This should be removed when the upstream has fixed the issue.
|
||||
# The device logic is to change the settings without turning it on
|
||||
# except when transition is defined, so we leverage that here for now.
|
||||
transition = 1
|
||||
|
||||
# Handle turning to temp mode
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP]))
|
||||
@@ -92,7 +110,9 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
@async_refresh_after
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
await self.device.turn_off(transition=kwargs.get(ATTR_TRANSITION))
|
||||
if (transition := kwargs.get(ATTR_TRANSITION)) is not None:
|
||||
transition = int(transition * 1_000)
|
||||
await self.device.turn_off(transition=transition)
|
||||
|
||||
@property
|
||||
def min_mireds(self) -> int:
|
||||
@@ -145,7 +165,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
def color_mode(self) -> str | None:
|
||||
"""Return the active color mode."""
|
||||
if self.device.is_color:
|
||||
if self.device.color_temp:
|
||||
if self.device.is_variable_color_temp and self.device.color_temp:
|
||||
return COLOR_MODE_COLOR_TEMP
|
||||
return COLOR_MODE_HS
|
||||
if self.device.is_variable_color_temp:
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -65,7 +67,9 @@ def async_migrate_legacy_entries(
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_yaml_entries(hass: HomeAssistant, conf: ConfigType) -> None:
|
||||
def async_migrate_yaml_entries(
|
||||
hass: HomeAssistant, conf: ConfigType | MappingProxyType[str, Any]
|
||||
) -> None:
|
||||
"""Migrate yaml to config entries."""
|
||||
for device_type in (CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER):
|
||||
for device in conf.get(device_type, []):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "UPnP/IGD",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/upnp",
|
||||
"requirements": ["async-upnp-client==0.22.5"],
|
||||
"requirements": ["async-upnp-client==0.22.8"],
|
||||
"dependencies": ["network", "ssdp"],
|
||||
"codeowners": ["@StevenLooman","@ehendrix23"],
|
||||
"ssdp": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -146,10 +146,14 @@ class XiaomiAirHumidifierSelector(XiaomiSelector):
|
||||
@callback
|
||||
def _handle_coordinator_update(self):
|
||||
"""Fetch state from the device."""
|
||||
self._current_led_brightness = self._extract_value_from_attribute(
|
||||
led_brightness = self._extract_value_from_attribute(
|
||||
self.coordinator.data, self.entity_description.key
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
# Sometimes (quite rarely) the device returns None as the LED brightness so we
|
||||
# check that the value is not None before updating the state.
|
||||
if led_brightness:
|
||||
self._current_led_brightness = led_brightness
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def current_option(self):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "yeelight",
|
||||
"name": "Yeelight",
|
||||
"documentation": "https://www.home-assistant.io/integrations/yeelight",
|
||||
"requirements": ["yeelight==0.7.6", "async-upnp-client==0.22.5"],
|
||||
"requirements": ["yeelight==0.7.7", "async-upnp-client==0.22.8"],
|
||||
"codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -408,7 +408,7 @@ class ZWaveServices:
|
||||
async def async_set_value(self, service: ServiceCall) -> None:
|
||||
"""Set a value on a node."""
|
||||
# pylint: disable=no-self-use
|
||||
nodes = service.data[const.ATTR_NODES]
|
||||
nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
|
||||
command_class = service.data[const.ATTR_COMMAND_CLASS]
|
||||
property_ = service.data[const.ATTR_PROPERTY]
|
||||
property_key = service.data.get(const.ATTR_PROPERTY_KEY)
|
||||
@@ -418,15 +418,27 @@ class ZWaveServices:
|
||||
options = service.data.get(const.ATTR_OPTIONS)
|
||||
|
||||
for node in nodes:
|
||||
value_id = get_value_id(
|
||||
node,
|
||||
command_class,
|
||||
property_,
|
||||
endpoint=endpoint,
|
||||
property_key=property_key,
|
||||
)
|
||||
# If value has a string type but the new value is not a string, we need to
|
||||
# convert it to one. We use new variable `new_value_` to convert the data
|
||||
# so we can preserve the original `new_value` for every node.
|
||||
if (
|
||||
value_id in node.values
|
||||
and node.values[value_id].metadata.type == "string"
|
||||
and not isinstance(new_value, str)
|
||||
):
|
||||
new_value_ = str(new_value)
|
||||
else:
|
||||
new_value_ = new_value
|
||||
success = await node.async_set_value(
|
||||
get_value_id(
|
||||
node,
|
||||
command_class,
|
||||
property_,
|
||||
endpoint=endpoint,
|
||||
property_key=property_key,
|
||||
),
|
||||
new_value,
|
||||
value_id,
|
||||
new_value_,
|
||||
options=options,
|
||||
wait_for_result=wait_for_result,
|
||||
)
|
||||
@@ -452,11 +464,16 @@ class ZWaveServices:
|
||||
await self.async_set_value(service)
|
||||
return
|
||||
|
||||
command_class = service.data[const.ATTR_COMMAND_CLASS]
|
||||
property_ = service.data[const.ATTR_PROPERTY]
|
||||
property_key = service.data.get(const.ATTR_PROPERTY_KEY)
|
||||
endpoint = service.data.get(const.ATTR_ENDPOINT)
|
||||
|
||||
value = {
|
||||
"commandClass": service.data[const.ATTR_COMMAND_CLASS],
|
||||
"property": service.data[const.ATTR_PROPERTY],
|
||||
"propertyKey": service.data.get(const.ATTR_PROPERTY_KEY),
|
||||
"endpoint": service.data.get(const.ATTR_ENDPOINT),
|
||||
"commandClass": command_class,
|
||||
"property": property_,
|
||||
"propertyKey": property_key,
|
||||
"endpoint": endpoint,
|
||||
}
|
||||
new_value = service.data[const.ATTR_VALUE]
|
||||
|
||||
@@ -464,12 +481,30 @@ class ZWaveServices:
|
||||
# schema validation and can use that to get the client, otherwise we can just
|
||||
# get the client from the node.
|
||||
client: ZwaveClient = None
|
||||
first_node = next((node for node in nodes), None)
|
||||
first_node: ZwaveNode = next((node for node in nodes), None)
|
||||
if first_node:
|
||||
client = first_node.client
|
||||
else:
|
||||
entry_id = self._hass.config_entries.async_entries(const.DOMAIN)[0].entry_id
|
||||
client = self._hass.data[const.DOMAIN][entry_id][const.DATA_CLIENT]
|
||||
first_node = next(
|
||||
node
|
||||
for node in client.driver.controller.nodes.values()
|
||||
if get_value_id(node, command_class, property_, endpoint, property_key)
|
||||
in node.values
|
||||
)
|
||||
|
||||
# If value has a string type but the new value is not a string, we need to
|
||||
# convert it to one
|
||||
value_id = get_value_id(
|
||||
first_node, command_class, property_, endpoint, property_key
|
||||
)
|
||||
if (
|
||||
value_id in first_node.values
|
||||
and first_node.values[value_id].metadata.type == "string"
|
||||
and not isinstance(new_value, str)
|
||||
):
|
||||
new_value = str(new_value)
|
||||
|
||||
success = await async_multicast_set_value(
|
||||
client=client,
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Final
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 10
|
||||
PATCH_VERSION: Final = "0b9"
|
||||
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)
|
||||
|
||||
@@ -4,7 +4,7 @@ aiodiscover==1.4.4
|
||||
aiohttp==3.7.4.post0
|
||||
aiohttp_cors==0.7.0
|
||||
astral==2.2
|
||||
async-upnp-client==0.22.5
|
||||
async-upnp-client==0.22.8
|
||||
async_timeout==3.0.1
|
||||
attrs==21.2.0
|
||||
awesomeversion==21.8.1
|
||||
@@ -15,7 +15,7 @@ ciso8601==2.2.0
|
||||
cryptography==3.4.8
|
||||
emoji==1.5.0
|
||||
hass-nabucasa==0.50.0
|
||||
home-assistant-frontend==20211004.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
|
||||
|
||||
|
||||
+10
-10
@@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2
|
||||
# Adafruit_BBIO==1.1.1
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.2.1
|
||||
HAP-python==4.3.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==1.5.1
|
||||
@@ -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
|
||||
@@ -219,7 +219,7 @@ aiomodernforms==0.1.8
|
||||
aiomusiccast==0.9.2
|
||||
|
||||
# homeassistant.components.nanoleaf
|
||||
aionanoleaf==0.0.2
|
||||
aionanoleaf==0.0.3
|
||||
|
||||
# homeassistant.components.keyboard_remote
|
||||
aionotify==0.2.0
|
||||
@@ -330,7 +330,7 @@ asterisk_mbox==0.5.0
|
||||
# homeassistant.components.ssdp
|
||||
# homeassistant.components.upnp
|
||||
# homeassistant.components.yeelight
|
||||
async-upnp-client==0.22.5
|
||||
async-upnp-client==0.22.8
|
||||
|
||||
# homeassistant.components.supla
|
||||
asyncpysupla==0.0.5
|
||||
@@ -810,7 +810,7 @@ hole==0.5.1
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211004.0
|
||||
home-assistant-frontend==20211007.1
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -1005,7 +1005,7 @@ micloud==0.3
|
||||
miflora==0.7.0
|
||||
|
||||
# homeassistant.components.mill
|
||||
millheater==0.6.0
|
||||
millheater==0.6.1
|
||||
|
||||
# homeassistant.components.minio
|
||||
minio==4.0.9
|
||||
@@ -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
|
||||
@@ -2459,7 +2459,7 @@ yalesmartalarmclient==0.3.4
|
||||
yalexs==1.1.13
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.6
|
||||
yeelight==0.7.7
|
||||
|
||||
# homeassistant.components.yeelightsunflower
|
||||
yeelightsunflower==0.0.10
|
||||
@@ -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
|
||||
|
||||
+10
-10
@@ -7,7 +7,7 @@
|
||||
AEMET-OpenData==0.2.1
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.2.1
|
||||
HAP-python==4.3.0
|
||||
|
||||
# homeassistant.components.flick_electric
|
||||
PyFlick==0.0.2
|
||||
@@ -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
|
||||
@@ -146,7 +146,7 @@ aiomodernforms==0.1.8
|
||||
aiomusiccast==0.9.2
|
||||
|
||||
# homeassistant.components.nanoleaf
|
||||
aionanoleaf==0.0.2
|
||||
aionanoleaf==0.0.3
|
||||
|
||||
# homeassistant.components.notion
|
||||
aionotion==3.0.2
|
||||
@@ -224,7 +224,7 @@ arcam-fmj==0.7.0
|
||||
# homeassistant.components.ssdp
|
||||
# homeassistant.components.upnp
|
||||
# homeassistant.components.yeelight
|
||||
async-upnp-client==0.22.5
|
||||
async-upnp-client==0.22.8
|
||||
|
||||
# homeassistant.components.aurora
|
||||
auroranoaa==0.0.2
|
||||
@@ -485,7 +485,7 @@ hole==0.5.1
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211004.0
|
||||
home-assistant-frontend==20211007.1
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -585,7 +585,7 @@ mficlient==0.3.0
|
||||
micloud==0.3
|
||||
|
||||
# homeassistant.components.mill
|
||||
millheater==0.6.0
|
||||
millheater==0.6.1
|
||||
|
||||
# homeassistant.components.minio
|
||||
minio==4.0.9
|
||||
@@ -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
|
||||
@@ -1403,13 +1403,13 @@ yalesmartalarmclient==0.3.4
|
||||
yalexs==1.1.13
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.6
|
||||
yeelight==0.7.7
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==0.13
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.36.7
|
||||
zeroconf==0.36.8
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.62
|
||||
|
||||
@@ -20,7 +20,7 @@ MOCK_SETTINGS = {
|
||||
"device": {"mac": "test-mac", "hostname": "test-host"},
|
||||
}
|
||||
DISCOVERY_INFO = {
|
||||
"host": "1.1.1.1",
|
||||
"host": ["169.1.1.1", "1.1.1.1"],
|
||||
"port": 0,
|
||||
"hostname": "shc012345.local.",
|
||||
"type": "_http._tcp.local.",
|
||||
@@ -526,6 +526,28 @@ async def test_zeroconf_cannot_connect(hass, mock_zeroconf):
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_zeroconf_link_local(hass, mock_zeroconf):
|
||||
"""Test we get the form."""
|
||||
DISCOVERY_INFO_LINK_LOCAL = {
|
||||
"host": ["169.1.1.1"],
|
||||
"port": 0,
|
||||
"hostname": "shc012345.local.",
|
||||
"type": "_http._tcp.local.",
|
||||
"name": "Bosch SHC [test-mac]._http._tcp.local.",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"boschshcpy.session.SHCSession.mdns_info", side_effect=SHCConnectionError
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=DISCOVERY_INFO_LINK_LOCAL,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
)
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_zeroconf_not_bosch_shc(hass, mock_zeroconf):
|
||||
"""Test we filter out non-bosch_shc devices."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
import asyncio
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp.web_exceptions import HTTPForbidden
|
||||
from aiohttp import ClientError, web_exceptions
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.daikin.const import KEY_MAC
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.data_entry_flow import (
|
||||
RESULT_TYPE_ABORT,
|
||||
RESULT_TYPE_CREATE_ENTRY,
|
||||
@@ -84,8 +83,8 @@ async def test_abort_if_already_setup(hass, mock_daikin):
|
||||
"s_effect,reason",
|
||||
[
|
||||
(asyncio.TimeoutError, "cannot_connect"),
|
||||
(HTTPForbidden, "invalid_auth"),
|
||||
(ClientError, "unknown"),
|
||||
(ClientError, "cannot_connect"),
|
||||
(web_exceptions.HTTPForbidden, "invalid_auth"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
@@ -103,6 +102,18 @@ async def test_device_abort(hass, mock_daikin, s_effect, reason):
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_api_password_abort(hass):
|
||||
"""Test device abort."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"daikin",
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: HOST, CONF_API_KEY: "aa", CONF_PASSWORD: "aa"},
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "api_password"}
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source, data, unique_id",
|
||||
[
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -96,6 +96,106 @@ async def setup_comp(hass, config_count):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)])
|
||||
async def test_state(hass, setup_comp):
|
||||
"""Test handling of state."""
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
# No entity has a valid state -> group state unknown
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME
|
||||
assert state.attributes[ATTR_ENTITY_ID] == [
|
||||
DEMO_COVER,
|
||||
DEMO_COVER_POS,
|
||||
DEMO_COVER_TILT,
|
||||
DEMO_TILT,
|
||||
]
|
||||
assert ATTR_ASSUMED_STATE not in state.attributes
|
||||
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
|
||||
assert ATTR_CURRENT_POSITION not in state.attributes
|
||||
assert ATTR_CURRENT_TILT_POSITION not in state.attributes
|
||||
|
||||
# Set all entities as closed -> group state closed
|
||||
hass.states.async_set(DEMO_COVER, STATE_CLOSED, {})
|
||||
hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {})
|
||||
hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {})
|
||||
hass.states.async_set(DEMO_TILT, STATE_CLOSED, {})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.state == STATE_CLOSED
|
||||
|
||||
# Set all entities as open -> group state open
|
||||
hass.states.async_set(DEMO_COVER, STATE_OPEN, {})
|
||||
hass.states.async_set(DEMO_COVER_POS, STATE_OPEN, {})
|
||||
hass.states.async_set(DEMO_COVER_TILT, STATE_OPEN, {})
|
||||
hass.states.async_set(DEMO_TILT, STATE_OPEN, {})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.state == STATE_OPEN
|
||||
|
||||
# Set first entity as open -> group state open
|
||||
hass.states.async_set(DEMO_COVER, STATE_OPEN, {})
|
||||
hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {})
|
||||
hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {})
|
||||
hass.states.async_set(DEMO_TILT, STATE_CLOSED, {})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.state == STATE_OPEN
|
||||
|
||||
# Set last entity as open -> group state open
|
||||
hass.states.async_set(DEMO_COVER, STATE_OPEN, {})
|
||||
hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {})
|
||||
hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {})
|
||||
hass.states.async_set(DEMO_TILT, STATE_CLOSED, {})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.state == STATE_OPEN
|
||||
|
||||
# Set conflicting valid states -> opening state has priority
|
||||
hass.states.async_set(DEMO_COVER, STATE_OPEN, {})
|
||||
hass.states.async_set(DEMO_COVER_POS, STATE_OPENING, {})
|
||||
hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSING, {})
|
||||
hass.states.async_set(DEMO_TILT, STATE_CLOSED, {})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.state == STATE_OPENING
|
||||
|
||||
# Set all entities to unknown state -> group state unknown
|
||||
hass.states.async_set(DEMO_COVER, STATE_UNKNOWN, {})
|
||||
hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {})
|
||||
hass.states.async_set(DEMO_COVER_TILT, STATE_UNKNOWN, {})
|
||||
hass.states.async_set(DEMO_TILT, STATE_UNKNOWN, {})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Set one entity to unknown state -> open state has priority
|
||||
hass.states.async_set(DEMO_COVER, STATE_OPEN, {})
|
||||
hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {})
|
||||
hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {})
|
||||
hass.states.async_set(DEMO_TILT, STATE_OPEN, {})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.state == STATE_OPEN
|
||||
|
||||
# Set one entity to unknown state -> opening state has priority
|
||||
hass.states.async_set(DEMO_COVER, STATE_OPEN, {})
|
||||
hass.states.async_set(DEMO_COVER_POS, STATE_OPENING, {})
|
||||
hass.states.async_set(DEMO_COVER_TILT, STATE_UNKNOWN, {})
|
||||
hass.states.async_set(DEMO_TILT, STATE_CLOSED, {})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.state == STATE_OPENING
|
||||
|
||||
# Set one entity to unknown state -> closing state has priority
|
||||
hass.states.async_set(DEMO_COVER, STATE_OPEN, {})
|
||||
hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {})
|
||||
hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSING, {})
|
||||
hass.states.async_set(DEMO_TILT, STATE_CLOSED, {})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.state == STATE_CLOSING
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)])
|
||||
async def test_attributes(hass, setup_comp):
|
||||
"""Test handling of state attributes."""
|
||||
@@ -196,7 +296,7 @@ async def test_attributes(hass, setup_comp):
|
||||
# ### Test assumed state ###
|
||||
# ##########################
|
||||
|
||||
# For covers
|
||||
# For covers - assumed state set true if position differ
|
||||
hass.states.async_set(
|
||||
DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100}
|
||||
)
|
||||
@@ -220,7 +320,7 @@ async def test_attributes(hass, setup_comp):
|
||||
assert ATTR_CURRENT_POSITION not in state.attributes
|
||||
assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 60
|
||||
|
||||
# For tilts
|
||||
# For tilts - assumed state set true if tilt position differ
|
||||
hass.states.async_set(
|
||||
DEMO_TILT,
|
||||
STATE_OPEN,
|
||||
@@ -252,6 +352,7 @@ async def test_attributes(hass, setup_comp):
|
||||
state = hass.states.get(COVER_GROUP)
|
||||
assert state.attributes[ATTR_ASSUMED_STATE] is True
|
||||
|
||||
# Test entity registry integration
|
||||
entity_registry = er.async_get(hass)
|
||||
entry = entity_registry.async_get(COVER_GROUP)
|
||||
assert entry
|
||||
|
||||
@@ -297,7 +297,7 @@ async def test_fan_speed(hass, hk_driver, events):
|
||||
)
|
||||
await hass.async_add_executor_job(acc.char_speed.client_update_value, 42)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_speed.value == 42
|
||||
assert acc.char_speed.value == 50
|
||||
assert acc.char_active.value == 1
|
||||
|
||||
assert call_set_percentage[0]
|
||||
@@ -309,7 +309,7 @@ async def test_fan_speed(hass, hk_driver, events):
|
||||
# Verify speed is preserved from off to on
|
||||
hass.states.async_set(entity_id, STATE_OFF, {ATTR_PERCENTAGE: 42})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_speed.value == 42
|
||||
assert acc.char_speed.value == 50
|
||||
assert acc.char_active.value == 0
|
||||
|
||||
hk_driver.set_characteristics(
|
||||
@@ -325,7 +325,7 @@ async def test_fan_speed(hass, hk_driver, events):
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_speed.value == 42
|
||||
assert acc.char_speed.value == 50
|
||||
assert acc.char_active.value == 1
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Test different accessory types: Media Players."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homekit.const import (
|
||||
ATTR_KEY_NAME,
|
||||
ATTR_VALUE,
|
||||
@@ -353,8 +355,9 @@ async def test_media_player_television(hass, hk_driver, events, caplog):
|
||||
|
||||
hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener)
|
||||
|
||||
await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20)
|
||||
await hass.async_block_till_done()
|
||||
with pytest.raises(ValueError):
|
||||
await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 7)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Test different accessory types: Remotes."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homekit.const import (
|
||||
ATTR_KEY_NAME,
|
||||
ATTR_VALUE,
|
||||
@@ -140,8 +142,9 @@ async def test_activity_remote(hass, hk_driver, events, caplog):
|
||||
|
||||
hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener)
|
||||
|
||||
acc.char_remote_key.client_update_value(20)
|
||||
await hass.async_block_till_done()
|
||||
with pytest.raises(ValueError):
|
||||
acc.char_remote_key.client_update_value(20)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
acc.char_remote_key.client_update_value(7)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1746,11 +1746,7 @@ async def test_water_heater(hass, hk_driver, events):
|
||||
assert len(events) == 1
|
||||
assert events[-1].data[ATTR_VALUE] == f"52.0{TEMP_CELSIUS}"
|
||||
|
||||
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 0)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_target_heat_cool.value == 1
|
||||
|
||||
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 2)
|
||||
await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 1)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_target_heat_cool.value == 1
|
||||
|
||||
|
||||
@@ -744,3 +744,30 @@ async def test_timestamp(hass):
|
||||
|
||||
finally:
|
||||
dt_util.set_default_time_zone(ORIG_TIMEZONE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config, error",
|
||||
[
|
||||
(
|
||||
{"has_time": True, "has_date": True, "initial": "abc"},
|
||||
"'abc' can't be parsed as a datetime",
|
||||
),
|
||||
(
|
||||
{"has_time": False, "has_date": True, "initial": "abc"},
|
||||
"'abc' can't be parsed as a date",
|
||||
),
|
||||
(
|
||||
{"has_time": True, "has_date": False, "initial": "abc"},
|
||||
"'abc' can't be parsed as a time",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_invalid_initial(hass, caplog, config, error):
|
||||
"""Test configuration is rejected if the initial value is invalid."""
|
||||
assert not await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{DOMAIN: {"test_date": config}},
|
||||
)
|
||||
assert error in caplog.text
|
||||
|
||||
@@ -6,7 +6,7 @@ from homeassistant.components.sensor import (
|
||||
ATTR_STATE_CLASS,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
STATE_CLASS_TOTAL,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
@@ -74,7 +74,7 @@ async def test_sensor_type_output(hass, mock_iotawatt):
|
||||
state = hass.states.get("sensor.my_watthour_sensor")
|
||||
assert state is not None
|
||||
assert state.state == "243"
|
||||
assert ATTR_STATE_CLASS not in state.attributes
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Sensor"
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
|
||||
@@ -125,7 +125,7 @@ async def test_sensor_type_accumulated_output(hass, mock_iotawatt):
|
||||
state.attributes[ATTR_FRIENDLY_NAME]
|
||||
== "My WattHour Accumulated Output Sensor.wh Accumulated"
|
||||
)
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
|
||||
assert state.attributes["type"] == "Output"
|
||||
@@ -166,7 +166,7 @@ async def test_sensor_type_accumulated_output_error_restore(hass, mock_iotawatt)
|
||||
state.attributes[ATTR_FRIENDLY_NAME]
|
||||
== "My WattHour Accumulated Output Sensor.wh Accumulated"
|
||||
)
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
|
||||
assert state.attributes["type"] == "Output"
|
||||
@@ -224,7 +224,7 @@ async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt):
|
||||
state.attributes[ATTR_FRIENDLY_NAME]
|
||||
== "My WattHour Accumulated Output Sensor.wh Accumulated"
|
||||
)
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING
|
||||
assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
|
||||
assert state.attributes["type"] == "Output"
|
||||
|
||||
@@ -23,9 +23,9 @@ from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
def _ssdp_headers(headers):
|
||||
return CaseInsensitiveDict(
|
||||
headers, _timestamp=datetime(2021, 1, 1, 12, 00), _udn=udn_from_headers(headers)
|
||||
)
|
||||
ssdp_headers = CaseInsensitiveDict(headers, _timestamp=datetime(2021, 1, 1, 12, 00))
|
||||
ssdp_headers["_udn"] = udn_from_headers(ssdp_headers)
|
||||
return ssdp_headers
|
||||
|
||||
|
||||
async def init_ssdp_component(hass: homeassistant) -> SsdpListener:
|
||||
@@ -45,7 +45,7 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow
|
||||
mock_ssdp_search_response = _ssdp_headers(
|
||||
{
|
||||
"st": "mock-st",
|
||||
"location": None,
|
||||
"location": "http://1.1.1.1",
|
||||
"usn": "uuid:mock-udn::mock-st",
|
||||
"server": "mock-server",
|
||||
"ext": "",
|
||||
@@ -64,7 +64,7 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow
|
||||
}
|
||||
assert mock_flow_init.mock_calls[0][2]["data"] == {
|
||||
ssdp.ATTR_SSDP_ST: "mock-st",
|
||||
ssdp.ATTR_SSDP_LOCATION: None,
|
||||
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
|
||||
ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st",
|
||||
ssdp.ATTR_SSDP_SERVER: "mock-server",
|
||||
ssdp.ATTR_SSDP_EXT: "",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,7 @@ def _mocked_bulb() -> SmartBulb:
|
||||
bulb.is_color = True
|
||||
bulb.is_strip = False
|
||||
bulb.is_plug = False
|
||||
bulb.is_dimmer = False
|
||||
bulb.hsv = (10, 30, 5)
|
||||
bulb.device_id = MAC_ADDRESS
|
||||
bulb.valid_temperature_range.min = 4000
|
||||
@@ -47,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()
|
||||
|
||||
@@ -1,27 +1,50 @@
|
||||
"""Tests for the TP-Link component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
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
|
||||
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
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_configuring_tplink_causes_discovery(hass):
|
||||
"""Test that specifying empty config does discovery."""
|
||||
with patch("homeassistant.components.tplink.Discover.discover") as discover:
|
||||
discover.return_value = {"host": 1234}
|
||||
discover.return_value = {MagicMock(): MagicMock()}
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
call_count = len(discover.mock_calls)
|
||||
assert discover.mock_calls
|
||||
|
||||
assert len(discover.mock_calls) == 1
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(discover.mock_calls) == call_count * 2
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15))
|
||||
await hass.async_block_till_done()
|
||||
assert len(discover.mock_calls) == call_count * 3
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30))
|
||||
await hass.async_block_till_done()
|
||||
assert len(discover.mock_calls) == call_count * 4
|
||||
|
||||
|
||||
async def test_config_entry_reload(hass):
|
||||
@@ -49,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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""Tests for light platform."""
|
||||
|
||||
from typing import Optional
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import tplink
|
||||
@@ -12,6 +15,7 @@ from homeassistant.components.light import (
|
||||
ATTR_MIN_MIREDS,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
ATTR_TRANSITION,
|
||||
ATTR_XY_COLOR,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
)
|
||||
@@ -43,8 +47,9 @@ async def test_light_unique_id(hass: HomeAssistant) -> None:
|
||||
assert entity_registry.async_get(entity_id).unique_id == "AABBCCDDEEFF"
|
||||
|
||||
|
||||
async def test_color_light(hass: HomeAssistant) -> None:
|
||||
"""Test a light."""
|
||||
@pytest.mark.parametrize("transition", [2.0, None])
|
||||
async def test_color_light(hass: HomeAssistant, transition: Optional[float]) -> None:
|
||||
"""Test a color light and that all transitions are correctly passed."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
@@ -56,6 +61,11 @@ async def test_color_light(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
KASA_TRANSITION_VALUE = transition * 1_000 if transition is not None else None
|
||||
|
||||
BASE_PAYLOAD = {ATTR_ENTITY_ID: entity_id}
|
||||
if transition:
|
||||
BASE_PAYLOAD[ATTR_TRANSITION] = transition
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
@@ -69,6 +79,81 @@ async def test_color_light(hass: HomeAssistant) -> None:
|
||||
assert attributes[ATTR_RGB_COLOR] == (255, 191, 178)
|
||||
assert attributes[ATTR_XY_COLOR] == (0.42, 0.336)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True
|
||||
)
|
||||
bulb.turn_off.assert_called_once_with(transition=KASA_TRANSITION_VALUE)
|
||||
|
||||
await hass.services.async_call(LIGHT_DOMAIN, "turn_on", BASE_PAYLOAD, blocking=True)
|
||||
bulb.turn_on.assert_called_once_with(transition=KASA_TRANSITION_VALUE)
|
||||
bulb.turn_on.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{**BASE_PAYLOAD, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE)
|
||||
bulb.set_brightness.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{**BASE_PAYLOAD, ATTR_COLOR_TEMP: 150},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(
|
||||
6666, brightness=None, transition=KASA_TRANSITION_VALUE
|
||||
)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{**BASE_PAYLOAD, ATTR_COLOR_TEMP: 150},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(
|
||||
6666, brightness=None, transition=KASA_TRANSITION_VALUE
|
||||
)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{**BASE_PAYLOAD, ATTR_HS_COLOR: (10, 30)},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE)
|
||||
bulb.set_hsv.reset_mock()
|
||||
|
||||
|
||||
async def test_color_light_no_temp(hass: HomeAssistant) -> None:
|
||||
"""Test a light."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.is_variable_color_temp = False
|
||||
type(bulb).color_temp = PropertyMock(side_effect=Exception)
|
||||
with _patch_discovery(device=bulb), _patch_single_discovery(device=bulb):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "on"
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 128
|
||||
assert attributes[ATTR_COLOR_MODE] == "hs"
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness", "hs"]
|
||||
assert attributes[ATTR_HS_COLOR] == (10, 30)
|
||||
assert attributes[ATTR_RGB_COLOR] == (255, 191, 178)
|
||||
assert attributes[ATTR_XY_COLOR] == (0.42, 0.336)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
@@ -89,24 +174,6 @@ async def test_color_light(hass: HomeAssistant) -> None:
|
||||
bulb.set_brightness.assert_called_with(39, transition=None)
|
||||
bulb.set_brightness.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None)
|
||||
bulb.set_color_temp.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
@@ -282,3 +349,29 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None:
|
||||
assert state.state == "off"
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["onoff"]
|
||||
|
||||
|
||||
async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None:
|
||||
"""Test a light."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.is_dimmer = True
|
||||
bulb.is_on = False
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_single_discovery(device=bulb):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.my_bulb"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "off"
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.turn_on.assert_called_once_with(transition=1)
|
||||
bulb.turn_on.reset_mock()
|
||||
|
||||
@@ -239,3 +239,25 @@ async def test_migrate_from_yaml(hass: HomeAssistant):
|
||||
|
||||
assert migrated_entry is not None
|
||||
assert migrated_entry.data[CONF_HOST] == IP_ADDRESS
|
||||
|
||||
|
||||
async def test_migrate_from_legacy_entry(hass: HomeAssistant):
|
||||
"""Test migrate from legacy entry that was already imported from yaml."""
|
||||
data = {
|
||||
CONF_DISCOVERY: False,
|
||||
CONF_SWITCH: [{CONF_HOST: IP_ADDRESS}],
|
||||
}
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
with _patch_discovery(), _patch_single_discovery():
|
||||
await setup.async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
migrated_entry = None
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.unique_id == MAC_ADDRESS:
|
||||
migrated_entry = entry
|
||||
break
|
||||
|
||||
assert migrated_entry is not None
|
||||
assert migrated_entry.data[CONF_HOST] == IP_ADDRESS
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,6 +43,7 @@ from .common import (
|
||||
CLIMATE_DANFOSS_LC13_ENTITY,
|
||||
CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY,
|
||||
CLIMATE_RADIO_THERMOSTAT_ENTITY,
|
||||
SCHLAGE_BE469_LOCK_ENTITY,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -1021,6 +1022,51 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration):
|
||||
)
|
||||
|
||||
|
||||
async def test_set_value_string(
|
||||
hass, client, climate_danfoss_lc_13, lock_schlage_be469, integration
|
||||
):
|
||||
"""Test set_value service converts number to string when needed."""
|
||||
client.async_send_command.return_value = {"success": True}
|
||||
|
||||
# Test that number gets converted to a string when needed
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY,
|
||||
ATTR_COMMAND_CLASS: 99,
|
||||
ATTR_PROPERTY: "userCode",
|
||||
ATTR_PROPERTY_KEY: 1,
|
||||
ATTR_VALUE: 12345,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "node.set_value"
|
||||
assert args["nodeId"] == lock_schlage_be469.node_id
|
||||
assert args["valueId"] == {
|
||||
"commandClassName": "User Code",
|
||||
"commandClass": 99,
|
||||
"endpoint": 0,
|
||||
"property": "userCode",
|
||||
"propertyName": "userCode",
|
||||
"propertyKey": 1,
|
||||
"propertyKeyName": "1",
|
||||
"metadata": {
|
||||
"type": "string",
|
||||
"readable": True,
|
||||
"writeable": True,
|
||||
"minLength": 4,
|
||||
"maxLength": 10,
|
||||
"label": "User Code (1)",
|
||||
},
|
||||
"value": "**********",
|
||||
}
|
||||
assert args["value"] == "12345"
|
||||
|
||||
|
||||
async def test_set_value_options(hass, client, aeon_smart_switch_6, integration):
|
||||
"""Test set_value service with options."""
|
||||
await hass.services.async_call(
|
||||
@@ -1381,6 +1427,41 @@ async def test_multicast_set_value_options(
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
|
||||
async def test_multicast_set_value_string(
|
||||
hass,
|
||||
client,
|
||||
lock_id_lock_as_id150,
|
||||
lock_schlage_be469,
|
||||
integration,
|
||||
):
|
||||
"""Test multicast_set_value service converts number to string when needed."""
|
||||
client.async_send_command.return_value = {"success": True}
|
||||
|
||||
# Test that number gets converted to a string when needed
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_MULTICAST_SET_VALUE,
|
||||
{
|
||||
ATTR_BROADCAST: True,
|
||||
ATTR_COMMAND_CLASS: 99,
|
||||
ATTR_PROPERTY: "userCode",
|
||||
ATTR_PROPERTY_KEY: 1,
|
||||
ATTR_VALUE: 12345,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 1
|
||||
args = client.async_send_command.call_args[0][0]
|
||||
assert args["command"] == "broadcast_node.set_value"
|
||||
assert args["valueId"] == {
|
||||
"commandClass": 99,
|
||||
"property": "userCode",
|
||||
"propertyKey": 1,
|
||||
}
|
||||
assert args["value"] == "12345"
|
||||
|
||||
|
||||
async def test_ping(
|
||||
hass,
|
||||
client,
|
||||
|
||||
Reference in New Issue
Block a user