Compare commits

..

51 Commits

Author SHA1 Message Date
Paulus Schoutsen a3ae25efdb Merge pull request #57455 from home-assistant/rc 2021-10-10 21:14:45 -07:00
Paulus Schoutsen ce75c11033 Bumped version to 2021.10.3 2021-10-10 20:34:53 -07:00
Shay Levy 862527782e Fix netgear renamed mdi icons (#57431) 2021-10-10 20:34:46 -07:00
J. Nick Koston b102a45bdf Bump zeroconf to 0.36.8 (#57451)
- Changelog: https://github.com/jstasiak/python-zeroconf/releases/tag/0.36.8
2021-10-10 20:22:50 -07:00
Shay Levy fc5a2e3e06 Fix Shelly button type in roller mode (#57429) 2021-10-10 20:22:49 -07:00
Shay Levy c01549999a Fix Shelly button filter empty event (#57427) 2021-10-10 20:22:48 -07:00
J. Nick Koston 7ed50bb9e6 Restore yeelight workaround for failing to update state after on/off (#57400) 2021-10-10 20:22:48 -07:00
mvn23 975b48859e Fix opentherm_gw.set_clock time parameter name (#57398) 2021-10-10 20:22:47 -07:00
mvn23 e92adde417 Fix default parameter values for service opentherm_gw.set_clock (#57397) 2021-10-10 20:22:46 -07:00
J. Nick Koston d929faaeff Remove executor calls in isy994 as its fully async (#57394) 2021-10-10 20:22:46 -07:00
J. Nick Koston 8c44f9b501 Do all of dhcp scapy startup in the executor (#57392) 2021-10-10 20:22:45 -07:00
Bram Kragten 2e9bbe5219 Update frontend to 20211007.1 (#57385) 2021-10-10 20:22:44 -07:00
Joakim Sørensen a4b6b74997 Bump pyhaversion to 21.10.0 (#57377) 2021-10-10 20:22:43 -07:00
RenierM26 307a07611e Bump Switchbot library (#57367)
* Bump dependency to fix SB password problem.

* Bump API version.
2021-10-10 20:22:42 -07:00
MatthewFlamm 0f2b9a6d77 Bump pynws to 1.3.2 (#57361) 2021-10-10 20:22:42 -07:00
Shay Levy 1e48bef74e Fix Shelly RGB/W supported color mode detection (#57359) 2021-10-10 20:22:41 -07:00
J. Nick Koston 438bdcbd8b Use switch format unique ids for tplink dimmers (#57346) 2021-10-10 20:22:40 -07:00
Robert Hillis 67e5f4e823 Catch errors for efergy (#57326)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-10-10 20:22:39 -07:00
Paulus Schoutsen b110a5bbfc Merge pull request #57355 from home-assistant/rc 2021-10-08 15:47:54 -07:00
Paulus Schoutsen 3c2f3f2ade Bumped version to 2021.10.2 2021-10-08 14:58:39 -07:00
Paulus Schoutsen 1ea0891602 Guard for bad last reset (#57344) 2021-10-08 14:58:34 -07:00
Steven Looman 54acc5bade Fix multiple upnp/ssdp issues (#57314) 2021-10-08 14:58:19 -07:00
Erik Montnemery 0c886e2990 Improve state of cover groups (#57313)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-10-08 14:51:11 -07:00
Milan Meulemans f10a469d4f Upgrade aionanoleaf to 0.0.3 to fix deadlock (#57312) 2021-10-08 14:51:10 -07:00
J. Nick Koston 0358a13536 Migrate tplink hosts that were previously imported from yaml (#57308) 2021-10-08 14:51:10 -07:00
Milan Meulemans 1a78c461a0 Fix Nanoleaf light turn_off transition (#57305) 2021-10-08 14:51:09 -07:00
starkillerOG c6d506cb4f Netgear fix port and device model beeing overwritten (#57277)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2021-10-08 14:51:08 -07:00
Paulus Schoutsen 5bb4bc3d13 Merge pull request #57294 from home-assistant/rc 2021-10-07 20:57:08 -07:00
Paulus Schoutsen 387249b593 Bumped version to 2021.10.1 2021-10-07 20:19:53 -07:00
J. Nick Koston 1a376b25af Bump yeelight to 0.7.7 (#57290) 2021-10-07 20:19:46 -07:00
Teemu R ed4b44c126 Stopgap fix for inconsistent upstream API of tplink dimmers (#57285) 2021-10-07 20:19:45 -07:00
J. Nick Koston ef7f1ffddc Bump HAP-python to 4.30 (#57284) 2021-10-07 20:19:44 -07:00
Teemu R e3e64130f1 Fix transition handling for tplink lights (#57272)
* Fix transition handling for tplink light

* Apply suggestions from code review

* Test that all transitions are passed correctly

* Fix linting

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2021-10-07 20:19:44 -07:00
Bram Kragten 4e49e3a3fb Update frontend to 20211007.0 (#57268) 2021-10-07 20:19:43 -07:00
J. Nick Koston fd371f2887 Fix RGB only (no color temp) devices with tplink (#57267) 2021-10-07 20:19:42 -07:00
Daniel Hjelseth Høyer ec0256e27f Bump Mill library to 0.6.1 (#57261) 2021-10-07 20:19:41 -07:00
Erik Montnemery 425015eb8b Validate initial value for input_datetime (#57256) 2021-10-07 20:19:41 -07:00
Martin Hjelmare 0b26b15749 Fix netgear config flow import (#57253) 2021-10-07 20:19:40 -07:00
Erik Montnemery 06befe906b Correct SQL query generated by get_metadata_with_session (#57225)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2021-10-07 20:19:39 -07:00
J. Nick Koston bf4a3d8d35 Discover tplink devices periodically (#57221)
- These devices sometimes do not respond on the first try
  or may be subject to transient broadcast failures, or
  overloads. We now try discovery periodically once the
  integration has been loaded.

- We used to try this 4x at startup, but that solution
  seemed to aggressive as we want to be sure we pickup
  the devices after startup as well since the network
  will likely be more calm after startup.
2021-10-07 20:19:15 -07:00
Raman Gupta 691d8d6b80 Convert val to str when needed while calling zwave_js.set_value (#57216) 2021-10-07 20:16:20 -07:00
Maciej Bieniek 7f49e02a4d Update led brightness select state only if valid data is available, Xiaomi Miio integration (#57197)
* Update state if there is valid data

* Add comment
2021-10-07 20:16:19 -07:00
J. Nick Koston 7544ec2399 Recreate the powerwall session/object when attempting relogin (#56935) 2021-10-07 20:16:19 -07:00
Franck Nijhof 32889dbfbe Merge pull request #57179 from home-assistant/rc 2021-10-06 16:36:51 +02:00
Franck Nijhof 46394b50d8 Bumped version to 2021.10.0 2021-10-06 15:53:41 +02:00
Erik Montnemery 25fc479cd4 Correct migration to recorder schema 18 (#57165) 2021-10-06 15:52:20 +02:00
Bram Kragten 9636799dfb Update frontend to 20211006.0 (#57164) 2021-10-06 15:52:14 +02:00
Thomas Schamm e01e575092 Skip link local addresses in bosch_shc discovery step (#57074) 2021-10-06 15:52:11 +02:00
Fredrik Erlandsson 2fc9cdbe68 Update Daikin config_flow with better error handling (#57069) 2021-10-06 15:52:07 +02:00
starkillerOG 72b3bc13e4 Remove Netgear tracker link_rate check on Orbi (#57032)
* Netgear tracker: remove link_rate check on Orbi

* fix debug message

* Add orbi models

* check start of model in V2 check

* fix black
2021-10-06 15:52:04 +02:00
Jean-Yves Avenard 9e755fcc49 Change energy state class to STATE_CLASS_TOTAL (#56974) 2021-10-06 15:52:00 +02:00
65 changed files with 1099 additions and 224 deletions
@@ -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"
+6 -2
View File
@@ -276,6 +276,10 @@ class DHCPWatcher(WatcherBase):
self._sniffer.stop()
async def async_start(self):
"""Start watching for dhcp packets."""
await self.hass.async_add_executor_job(self._start)
def _start(self):
"""Start watching for dhcp packets."""
# Local import because importing from scapy has side effects such as opening
# sockets
@@ -319,7 +323,7 @@ class DHCPWatcher(WatcherBase):
conf.sniff_promisc = 0
try:
await self.hass.async_add_executor_job(_verify_l2socket_setup, FILTER)
_verify_l2socket_setup(FILTER)
except (Scapy_Exception, OSError) as ex:
if os.geteuid() == 0:
_LOGGER.error("Cannot watch for dhcp packets: %s", ex)
@@ -330,7 +334,7 @@ class DHCPWatcher(WatcherBase):
return
try:
await self.hass.async_add_executor_job(_verify_working_pcap, FILTER)
_verify_working_pcap(FILTER)
except (Scapy_Exception, ImportError) as ex:
_LOGGER.error(
"Cannot watch for dhcp packets without a functional packet filter: %s",
@@ -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"
+22 -5
View File
@@ -1,7 +1,9 @@
"""Support for Efergy sensors."""
from __future__ import annotations
from pyefergy import Efergy
import logging
from pyefergy import Efergy, exceptions
import voluptuous as vol
from homeassistant.components.sensor import (
@@ -20,6 +22,7 @@ from homeassistant.const import (
POWER_WATT,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -39,6 +42,8 @@ CONF_CURRENT_VALUES = "current_values"
DEFAULT_PERIOD = "year"
DEFAULT_UTC_OFFSET = "0"
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES: dict[str, SensorEntityDescription] = {
CONF_INSTANT: SensorEntityDescription(
key=CONF_INSTANT,
@@ -102,7 +107,10 @@ async def async_setup_platform(
)
dev = []
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",
+8 -2
View File
@@ -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,
)
)
},
+2 -2
View File
@@ -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"
+5 -8
View File
@@ -205,21 +205,18 @@ async def async_setup_entry(
# Load platforms for the devices in the ISY controller that we support.
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
def _start_auto_update() -> None:
"""Start isy auto update."""
_LOGGER.debug("ISY Starting Event Stream and automatic updates")
isy.websocket.start()
def _stop_auto_update(event) -> None:
@callback
def _async_stop_auto_update(event) -> None:
"""Stop the isy auto update on Home Assistant Shutdown."""
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
isy.websocket.stop()
await hass.async_add_executor_job(_start_auto_update)
_LOGGER.debug("ISY Starting Event Stream and automatic updates")
isy.websocket.start()
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_auto_update)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update)
)
# Register Integration-wide Services:
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -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
+26 -9
View File
@@ -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],
)
)
+8 -7
View File
@@ -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),
}
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "National Weather Service (NWS)",
"documentation": "https://www.home-assistant.io/integrations/nws",
"codeowners": ["@MatthewFlamm"],
"requirements": ["pynws==1.3.1"],
"requirements": ["pynws==1.3.2"],
"quality_scale": "platinum",
"config_flow": true,
"iot_class": "cloud_polling"
@@ -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:
+23 -10
View File
@@ -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,
}
)
+21 -26
View File
@@ -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 {}
+6 -4
View File
@@ -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
+1 -1
View File
@@ -275,7 +275,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator):
if block.type != "device":
continue
if block.wakeupEvent[0] == "button":
if len(block.wakeupEvent) == 1 and block.wakeupEvent[0] == "button":
self._last_input_events_count[1] = -1
break
+5
View File
@@ -21,6 +21,11 @@ LIGHT_TRANSITION_MIN_FIRMWARE_DATE: Final = 20210226
# max light transition time in milliseconds
MAX_TRANSITION_TIME: Final = 5000
RGBW_MODELS: Final = (
"SHBLB-1",
"SHRGBW2",
)
MODELS_SUPPORTING_LIGHT_TRANSITION: Final = (
"SHBDUO-1",
"SHCB-1",
+2 -1
View File
@@ -46,6 +46,7 @@ from .const import (
LIGHT_TRANSITION_MIN_FIRMWARE_DATE,
MAX_TRANSITION_TIME,
MODELS_SUPPORTING_LIGHT_TRANSITION,
RGBW_MODELS,
RPC,
SHBLB_1_RGB_EFFECTS,
STANDARD_RGB_EFFECTS,
@@ -143,7 +144,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity):
if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"):
self._min_kelvin = KELVIN_MIN_VALUE_COLOR
if hasattr(block, "white"):
if wrapper.model in RGBW_MODELS:
self._supported_color_modes.add(COLOR_MODE_RGBW)
else:
self._supported_color_modes.add(COLOR_MODE_RGB)
+4
View File
@@ -133,6 +133,10 @@ def is_block_momentary_input(settings: dict[str, Any], block: Block) -> bool:
if settings["device"]["type"] in SHBTN_MODELS:
return True
if settings.get("mode") == "roller":
button_type = settings["rollers"][0]["button_type"]
return button_type in ["momentary", "momentary_on_release"]
button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
if button is None:
return False
+1 -1
View File
@@ -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"
+65 -2
View File
@@ -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]
+24 -4
View File
@@ -26,6 +26,7 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin as mired_to_kelvin,
)
from . import legacy_device_id
from .const import DOMAIN
from .coordinator import TPLinkDataUpdateCoordinator
from .entity import CoordinatedTPLinkEntity, async_refresh_after
@@ -58,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:
+5 -1
View File
@@ -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, []):
+1 -1
View File
@@ -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
+41 -2
View File
@@ -38,6 +38,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_call_later
import homeassistant.util.color as color_util
from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired,
@@ -62,6 +63,7 @@ from . import (
DATA_DEVICE,
DATA_UPDATED,
DOMAIN,
POWER_STATE_CHANGE_TIME,
YEELIGHT_FLOW_TRANSITION_SCHEMA,
YeelightEntity,
)
@@ -247,7 +249,7 @@ def _async_cmd(func):
except BULB_NETWORK_EXCEPTIONS as ex:
# A network error happened, the bulb is likely offline now
self.device.async_mark_unavailable()
self.async_write_ha_state()
self.async_state_changed()
exc_message = str(ex) or type(ex)
raise HomeAssistantError(
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
@@ -419,13 +421,22 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
else:
self._custom_effects = {}
self._unexpected_state_check = None
@callback
def async_state_changed(self):
"""Call when the device changes state."""
if not self._device.available:
self._async_cancel_pending_state_check()
self.async_write_ha_state()
async def async_added_to_hass(self):
"""Handle entity which will be added."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
DATA_UPDATED.format(self._device.host),
self.async_write_ha_state,
self.async_state_changed,
)
)
await super().async_added_to_hass()
@@ -760,6 +771,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
await self.async_set_default()
self._async_schedule_state_check(True)
@callback
def _async_cancel_pending_state_check(self):
"""Cancel a pending state check."""
if self._unexpected_state_check:
self._unexpected_state_check()
self._unexpected_state_check = None
@callback
def _async_schedule_state_check(self, expected_power_state):
"""Schedule a poll if the change failed to get pushed back to us.
Some devices (mainly nightlights) will not send back the on state
so we need to force a refresh.
"""
self._async_cancel_pending_state_check()
async def _async_update_if_state_unexpected(*_):
self._unexpected_state_check = None
if self.is_on != expected_power_state:
await self.device.async_update(True)
self._unexpected_state_check = async_call_later(
self.hass, POWER_STATE_CHANGE_TIME, _async_update_if_state_unexpected
)
@_async_cmd
async def _async_turn_off(self, duration) -> None:
"""Turn off with a given transition duration wrapped with _async_cmd."""
@@ -775,6 +813,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
await self._async_turn_off(duration)
self._async_schedule_state_check(False)
@_async_cmd
async def async_set_mode(self, mode: str):
@@ -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",
+49 -14
View File
@@ -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,
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+23 -1
View File
@@ -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(
+16 -5
View File
@@ -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",
[
+52 -4
View File
@@ -1,9 +1,15 @@
"""The tests for Efergy sensor platform."""
import asyncio
from datetime import timedelta
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import load_fixture
from tests.common import async_fire_time_changed, load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT"
@@ -30,9 +36,14 @@ MULTI_SENSOR_CONFIG = {
}
def mock_responses(aioclient_mock: AiohttpClientMocker):
def mock_responses(aioclient_mock: AiohttpClientMocker, error: bool = False):
"""Mock responses for Efergy."""
base_url = "https://engage.efergy.com/mobile_proxy/"
if error:
aioclient_mock.get(
f"{base_url}getCurrentValuesSummary?token={token}", exc=asyncio.TimeoutError
)
return
aioclient_mock.get(
f"{base_url}getInstant?token={token}",
text=load_fixture("efergy/efergy_instant.json"),
@@ -64,7 +75,9 @@ async def test_single_sensor_readings(
):
"""Test for successfully setting up the Efergy platform."""
mock_responses(aioclient_mock)
assert await async_setup_component(hass, "sensor", {"sensor": ONE_SENSOR_CONFIG})
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
await hass.async_block_till_done()
assert hass.states.get("sensor.energy_consumed").state == "38.21"
@@ -79,9 +92,44 @@ async def test_multi_sensor_readings(
):
"""Test for multiple sensors in one household."""
mock_responses(aioclient_mock)
assert await async_setup_component(hass, "sensor", {"sensor": MULTI_SENSOR_CONFIG})
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MULTI_SENSOR_CONFIG}
)
await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == "218"
assert hass.states.get("sensor.efergy_0").state == "1808"
assert hass.states.get("sensor.efergy_728387").state == "312"
async def test_failed_getting_sids(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
):
"""Test failed gettings sids."""
mock_responses(aioclient_mock, error=True)
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
assert not hass.states.async_all("sensor")
async def test_failed_update_and_reconnection(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
):
"""Test failed update and reconnection."""
mock_responses(aioclient_mock)
assert await async_setup_component(
hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG}
)
aioclient_mock.clear_requests()
mock_responses(aioclient_mock, error=True)
next_update = dt_util.utcnow() + timedelta(seconds=3)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == STATE_UNAVAILABLE
aioclient_mock.clear_requests()
mock_responses(aioclient_mock)
next_update = dt_util.utcnow() + timedelta(seconds=30)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert hass.states.get("sensor.efergy_728386").state == "1628"
+103 -2
View File
@@ -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
+3 -3
View File
@@ -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()
+5 -2
View File
@@ -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
+5 -5
View File
@@ -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"
+5 -5
View File
@@ -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: "",
+29 -1
View File
@@ -2,7 +2,7 @@
from unittest.mock import AsyncMock, MagicMock, patch
from kasa import SmartBulb, SmartPlug, SmartStrip
from kasa import SmartBulb, SmartDimmer, SmartPlug, SmartStrip
from kasa.exceptions import SmartDeviceException
from kasa.protocol import TPLinkSmartHomeProtocol
@@ -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()
+99 -6
View File
@@ -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
+113 -20
View File
@@ -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()
+22
View File
@@ -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
+73 -1
View File
@@ -1,5 +1,6 @@
"""Test the Yeelight light."""
import asyncio
from datetime import timedelta
import logging
import socket
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
@@ -98,6 +99,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util.color import (
color_hs_to_RGB,
color_hs_to_xy,
@@ -121,7 +123,7 @@ from . import (
_patch_discovery_interval,
)
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
CONFIG_ENTRY_DATA = {
CONF_HOST: IP_ADDRESS,
@@ -1377,3 +1379,73 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant):
assert state.state == "on"
# bg_power off should not set the brightness to 0
assert state.attributes[ATTR_BRIGHTNESS] == 128
async def test_state_fails_to_update_triggers_update(hass: HomeAssistant):
"""Ensure we call async_get_properties if the turn on/off fails to update the state."""
mocked_bulb = _mocked_bulb()
properties = {**PROPERTIES}
properties.pop("active_mode")
properties["color_mode"] = "3" # HSV
mocked_bulb.last_properties = properties
mocked_bulb.bulb_type = BulbType.Color
config_entry = MockConfigEntry(
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
)
config_entry.add_to_hass(hass)
with _patch_discovery(), _patch_discovery_interval(), patch(
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# We use asyncio.create_task now to avoid
# blocking starting so we need to block again
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 1
mocked_bulb.last_properties["power"] = "off"
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
mocked_bulb.last_properties["power"] = "on"
for _ in range(5):
await hass.services.async_call(
"light",
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_off.mock_calls) == 5
# Even with five calls we only do one state request
# since each successive call should cancel the unexpected
# state check
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2))
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 3
# But if the state is correct no calls
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ENTITY_LIGHT,
},
blocking=True,
)
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
await hass.async_block_till_done()
assert len(mocked_bulb.async_get_properties.mock_calls) == 3
@@ -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,