Compare commits

..

40 Commits

Author SHA1 Message Date
Paulus Schoutsen
737c502e94 Merge pull request #67838 from home-assistant/rc 2022-03-07 21:51:30 -08:00
Paulus Schoutsen
a1abcbc7eb Bumped version to 2022.3.3 2022-03-07 20:45:49 -08:00
J. Nick Koston
b09ab2dafb Prevent scene from restoring unavailable states (#67836) 2022-03-07 20:45:44 -08:00
Teemu R
4e6fc3615b Bump python-miio version to 0.5.11 (#67824) 2022-03-07 20:45:43 -08:00
Bram Kragten
580c998552 Update frontend to 20220301.1 (#67812) 2022-03-07 20:45:25 -08:00
Franck Nijhof
97ba17d1ec Catch Elgato connection errors (#67799) 2022-03-07 20:44:09 -08:00
J. Nick Koston
8d7cdceb75 Handle fan_modes being set to None in homekit (#67790) 2022-03-07 20:44:08 -08:00
Simone Chemelli
dfa1c3abb3 Fix profile name update for Shelly Valve (#67778) 2022-03-07 20:44:08 -08:00
Simone Chemelli
c807c57a9b Fix internet access switch for old discovery (#67777) 2022-03-07 20:44:07 -08:00
J. Nick Koston
f4ec7e0902 Prevent polling from recreating an entity after removal (#67750) 2022-03-07 20:44:06 -08:00
G Johansson
814c96834e Fix temperature stepping in Sensibo (#67737)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-07 20:44:05 -08:00
muppet3000
87492e6b3e Fix timezone for growatt lastdataupdate (#67684)
* Added timezone for growatt lastdataupdate (#67646)

* Growatt lastdataupdate set to local timezone
2022-03-07 20:44:05 -08:00
Jan Bouwhuis
4aaafb0a99 Fix false positive MQTT climate deprecation warnings for defaults (#67661)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-03-07 20:44:04 -08:00
Paulus Schoutsen
2aecdd3d6d Merge pull request #67730 from home-assistant/rc 2022-03-06 12:11:16 -08:00
Jc2k
76336df91a Fix regression with homekit_controller + Aqara motion/vibration sensors (#67740) 2022-03-06 08:45:56 -08:00
Paulus Schoutsen
88e0380aa2 Bumped version to 2022.3.2 2022-03-06 00:07:45 -08:00
Avi Miller
10a2c97cab Update aiolifx dependency to resolve log flood (#67721) 2022-03-06 00:07:41 -08:00
J. Nick Koston
92c3c08a10 Add missing disconnect in elkm1 config flow validation (#67716) 2022-03-06 00:07:40 -08:00
J. Nick Koston
4f8b69d985 Ensure elkm1 can be manually configured when discovered instance is not used (#67712) 2022-03-06 00:07:39 -08:00
Martin Hjelmare
f5aaf44e50 Bump pydroid-ipcam to 1.3.1 (#67655)
* Bump pydroid-ipcam to 1.3.1

* Remove loop and set ssl to False
2022-03-06 00:07:39 -08:00
Erik Montnemery
f3c85b3459 Fix reload of media player groups (#67653) 2022-03-06 00:07:38 -08:00
Franck Nijhof
d7348718e0 Fix Fan template loosing percentage/preset (#67648)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-03-06 00:07:37 -08:00
Simone Chemelli
2a6d5ea7bd Improve logging for Fritz switches creation (#67640) 2022-03-06 00:07:37 -08:00
Simone Chemelli
5ae83e3c40 Allign logic for Fritz sensors and binary_sensors (#67623) 2022-03-06 00:07:36 -08:00
G Johansson
5657a9e6bd Fix sql false warning (#67614) 2022-03-06 00:07:35 -08:00
J. Nick Koston
b290e62170 Handle elkm1 login case with username and insecure login (#67602) 2022-03-06 00:07:35 -08:00
epenet
679ddbd1be Downgrade Renault warning (#67601)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-03-06 00:07:34 -08:00
Teemu R
b54652a849 Remove use of deprecated xiaomi_miio classes (#67590) 2022-03-06 00:07:33 -08:00
Joakim Plate
24013ad94c rfxtrx: bump to 0.28 (#67530) 2022-03-06 00:07:32 -08:00
Chris Talkington
9849b86a84 Suppress roku power off timeout errors (#67414) 2022-03-06 00:07:32 -08:00
Simone Chemelli
8bbf55c85d Add unique_id to Fritz diagnostics (#67384)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-06 00:07:31 -08:00
Paulus Schoutsen
0541c708da Merge pull request #67588 from home-assistant/rc 2022-03-03 18:49:44 -08:00
Paulus Schoutsen
ba40d62081 Bumped version to 2022.3.1 2022-03-03 15:53:54 -08:00
J. Nick Koston
73765a1f29 Add guards for HomeKit version/names that break apple watches (#67585) 2022-03-03 15:53:46 -08:00
muppet3000
b5b945ab4d Fix data type for growatt lastdataupdate (#67511) (#67582)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-03-03 15:53:46 -08:00
Emory Penney
d361643500 Bump pyobihai (#67571) 2022-03-03 15:53:45 -08:00
Paulus Schoutsen
eff7a12557 Highlight in logs it is a custom component when setup fails (#67559)
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
2022-03-03 15:53:44 -08:00
Jan Bouwhuis
63f8e9ee08 Fix MQTT config flow with advanced parameters (#67556)
* Fix MQTT config flow with advanced parameters

* Add test
2022-03-03 15:53:44 -08:00
Simone Chemelli
ee0bdaa2de Check if UPnP is enabled on Fritz device (#67512)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-03 15:48:24 -08:00
jjlawren
48d9e9a83c Bump soco to 0.26.4 (#67498) 2022-03-03 15:47:50 -08:00
65 changed files with 1037 additions and 279 deletions

View File

@@ -204,13 +204,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# Init ip webcam
cam = PyDroidIPCam(
hass.loop,
websession,
host,
cam_config[CONF_PORT],
username=username,
password=password,
timeout=cam_config[CONF_TIMEOUT],
ssl=False,
)
if switches is None:

View File

@@ -2,7 +2,7 @@
"domain": "android_ip_webcam",
"name": "Android IP Webcam",
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
"requirements": ["pydroid-ipcam==0.8"],
"requirements": ["pydroid-ipcam==1.3.1"],
"codeowners": [],
"iot_class": "local_polling"
}

View File

@@ -1,13 +1,13 @@
"""Support for Elgato Lights."""
from typing import NamedTuple
from elgato import Elgato, Info, State
from elgato import Elgato, ElgatoConnectionError, Info, State
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@@ -31,12 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session=session,
)
async def _async_update_data() -> State:
"""Fetch Elgato data."""
try:
return await elgato.state()
except ElgatoConnectionError as err:
raise UpdateFailed(err) from err
coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator(
hass,
LOGGER,
name=f"{DOMAIN}_{entry.data[CONF_HOST]}",
update_interval=SCAN_INTERVAL,
update_method=elgato.state,
update_method=_async_update_data,
)
await coordinator.async_config_entry_first_refresh()

View File

@@ -279,9 +279,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
keypad.add_callback(_element_changed)
try:
if not await async_wait_for_elk_to_sync(
elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME])
):
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT):
return False
except asyncio.TimeoutError as exc:
raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc
@@ -334,7 +332,6 @@ async def async_wait_for_elk_to_sync(
elk: elkm1.Elk,
login_timeout: int,
sync_timeout: int,
password_auth: bool,
) -> bool:
"""Wait until the elk has finished sync. Can fail login or timeout."""
@@ -354,18 +351,23 @@ async def async_wait_for_elk_to_sync(
login_event.set()
sync_event.set()
def first_response(*args, **kwargs):
_LOGGER.debug("ElkM1 received first response (VN)")
login_event.set()
def sync_complete():
sync_event.set()
success = True
elk.add_handler("login", login_status)
# VN is the first command sent for panel, when we get
# it back we now we are logged in either with or without a password
elk.add_handler("VN", first_response)
elk.add_handler("sync_complete", sync_complete)
events = []
if password_auth:
events.append(("login", login_event, login_timeout))
events.append(("sync_complete", sync_event, sync_timeout))
for name, event, timeout in events:
for name, event, timeout in (
("login", login_event, login_timeout),
("sync_complete", sync_event, sync_timeout),
):
_LOGGER.debug("Waiting for %s event for %s seconds", name, timeout)
try:
async with async_timeout.timeout(timeout):

View File

@@ -81,10 +81,11 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str
)
elk.connect()
if not await async_wait_for_elk_to_sync(
elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid)
):
raise InvalidAuth
try:
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT):
raise InvalidAuth
finally:
elk.disconnect()
short_mac = _short_mac(mac) if mac else None
if prefix and prefix != short_mac:
@@ -227,7 +228,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
info = await validate_input(user_input, self.unique_id)
except asyncio.TimeoutError:
return {CONF_HOST: "cannot_connect"}, None
return {"base": "cannot_connect"}, None
except InvalidAuth:
return {CONF_PASSWORD: "invalid_auth"}, None
except Exception: # pylint: disable=broad-except
@@ -287,9 +288,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if device := await async_discover_device(
self.hass, user_input[CONF_ADDRESS]
):
await self.async_set_unique_id(dr.format_mac(device.mac_address))
await self.async_set_unique_id(
dr.format_mac(device.mac_address), raise_on_progress=False
)
self._abort_if_unique_id_configured()
user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}"
# Ignore the port from discovery since its always going to be
# 2601 if secure is turned on even though they may want insecure
user_input[CONF_ADDRESS] = device.ip_address
errors, result = await self._async_create_or_error(user_input, False)
if not errors:
return result
@@ -324,7 +329,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if is_ip_address(host) and (
device := await async_discover_device(self.hass, host)
):
await self.async_set_unique_id(dr.format_mac(device.mac_address))
await self.async_set_unique_id(
dr.format_mac(device.mac_address), raise_on_progress=False
)
self._abort_if_unique_id_configured()
return (await self._async_create_or_error(user_input, True))[1]

View File

@@ -33,6 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except FRITZ_EXCEPTIONS as ex:
raise ConfigEntryNotReady from ex
if (
"X_AVM-DE_UPnP1" in avm_wrapper.connection.services
and not (await avm_wrapper.async_get_upnp_configuration())["NewEnable"]
):
raise ConfigEntryAuthFailed("Missing UPnP configuration")
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = avm_wrapper

View File

@@ -1,6 +1,7 @@
"""AVM FRITZ!Box connectivity sensor."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
@@ -14,8 +15,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .common import AvmWrapper, FritzBoxBaseEntity
from .const import DOMAIN, MeshRoles
from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -24,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
class FritzBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Fritz sensor entity."""
exclude_mesh_role: MeshRoles = MeshRoles.SLAVE
is_suitable: Callable[[ConnectionInfo], bool] = lambda info: info.wan_enabled
SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = (
@@ -45,7 +46,7 @@ SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = (
name="Firmware Update",
device_class=BinarySensorDeviceClass.UPDATE,
entity_category=EntityCategory.DIAGNOSTIC,
exclude_mesh_role=MeshRoles.NONE,
is_suitable=lambda info: True,
),
)
@@ -57,10 +58,12 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box binary sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
connection_info = await avm_wrapper.async_get_connection_info()
entities = [
FritzBoxBinarySensor(avm_wrapper, entry.title, description)
for description in SENSOR_TYPES
if (description.exclude_mesh_role != avm_wrapper.mesh_role)
if description.is_suitable(connection_info)
]
async_add_entities(entities, True)

View File

@@ -392,6 +392,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
)
self.mesh_role = MeshRoles.NONE
for mac, info in hosts.items():
if info.ip_address:
info.wan_access = self._get_wan_access(info.ip_address)
if self.manage_device_info(info, mac, consider_home):
new_device = True
self.send_signal_device_update(new_device)
@@ -630,6 +632,11 @@ class AvmWrapper(FritzBoxTools):
)
return {}
async def async_get_upnp_configuration(self) -> dict[str, Any]:
"""Call X_AVM-DE_UPnP service."""
return await self.hass.async_add_executor_job(self.get_upnp_configuration)
async def async_get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service."""
@@ -637,6 +644,22 @@ class AvmWrapper(FritzBoxTools):
partial(self.get_wan_link_properties)
)
async def async_get_connection_info(self) -> ConnectionInfo:
"""Return ConnectionInfo data."""
link_properties = await self.async_get_wan_link_properties()
connection_info = ConnectionInfo(
connection=link_properties.get("NewWANAccessType", "").lower(),
mesh_role=self.mesh_role,
wan_enabled=self.device_is_router,
)
_LOGGER.debug(
"ConnectionInfo for FritzBox %s: %s",
self.host,
connection_info,
)
return connection_info
async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]:
"""Call GetGenericPortMappingEntry action."""
@@ -698,6 +721,11 @@ class AvmWrapper(FritzBoxTools):
partial(self.set_allow_wan_access, ip_address, turn_on)
)
def get_upnp_configuration(self) -> dict[str, Any]:
"""Call X_AVM-DE_UPnP service."""
return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo")
def get_ontel_num_deflections(self) -> dict[str, Any]:
"""Call GetNumberOfDeflections action from X_AVM-DE_OnTel service."""
@@ -960,3 +988,12 @@ class FritzBoxBaseEntity:
name=self._device_name,
sw_version=self._avm_wrapper.current_firmware,
)
@dataclass
class ConnectionInfo:
"""Fritz sensor connection information class."""
connection: str
mesh_role: MeshRoles
wan_enabled: bool

View File

@@ -29,6 +29,7 @@ from .const import (
ERROR_AUTH_INVALID,
ERROR_CANNOT_CONNECT,
ERROR_UNKNOWN,
ERROR_UPNP_NOT_CONFIGURED,
)
_LOGGER = logging.getLogger(__name__)
@@ -79,6 +80,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
return ERROR_UNKNOWN
if (
"X_AVM-DE_UPnP1" in self.avm_wrapper.connection.services
and not (await self.avm_wrapper.async_get_upnp_configuration())["NewEnable"]
):
return ERROR_UPNP_NOT_CONFIGURED
return None
async def async_check_configured_entry(self) -> ConfigEntry | None:

View File

@@ -46,6 +46,7 @@ DEFAULT_USERNAME = ""
ERROR_AUTH_INVALID = "invalid_auth"
ERROR_CANNOT_CONNECT = "cannot_connect"
ERROR_UPNP_NOT_CONFIGURED = "upnp_not_configured"
ERROR_UNKNOWN = "unknown_error"
FRITZ_SERVICES = "fritz_services"
@@ -56,6 +57,7 @@ SERVICE_SET_GUEST_WIFI_PW = "set_guest_wifi_password"
SWITCH_TYPE_DEFLECTION = "CallDeflection"
SWITCH_TYPE_PORTFORWARD = "PortForward"
SWITCH_TYPE_PROFILE = "Profile"
SWITCH_TYPE_WIFINETWORK = "WiFiNetwork"
UPTIME_DEVIATION = 5

View File

@@ -22,6 +22,9 @@ async def async_get_config_entry_diagnostics(
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device_info": {
"model": avm_wrapper.model,
"unique_id": avm_wrapper.unique_id.replace(
avm_wrapper.unique_id[6:11], "XX:XX"
),
"current_firmware": avm_wrapper.current_firmware,
"latest_firmware": avm_wrapper.latest_firmware,
"update_available": avm_wrapper.update_available,

View File

@@ -28,8 +28,8 @@ from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
from .common import AvmWrapper, FritzBoxBaseEntity
from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION, MeshRoles
from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity
from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION
_LOGGER = logging.getLogger(__name__)
@@ -134,15 +134,6 @@ def _retrieve_link_attenuation_received_state(
return status.attenuation[1] / 10 # type: ignore[no-any-return]
@dataclass
class ConnectionInfo:
"""Fritz sensor connection information class."""
connection: str
mesh_role: MeshRoles
wan_enabled: bool
@dataclass
class FritzRequireKeysMixin:
"""Fritz sensor data class."""
@@ -283,18 +274,7 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
link_properties = await avm_wrapper.async_get_wan_link_properties()
connection_info = ConnectionInfo(
connection=link_properties.get("NewWANAccessType", "").lower(),
mesh_role=avm_wrapper.mesh_role,
wan_enabled=avm_wrapper.device_is_router,
)
_LOGGER.debug(
"ConnectionInfo for FritzBox %s: %s",
avm_wrapper.host,
connection_info,
)
connection_info = await avm_wrapper.async_get_connection_info()
entities = [
FritzBoxSensor(avm_wrapper, entry.title, description)

View File

@@ -36,6 +36,7 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"upnp_not_configured": "Missing UPnP settings on device.",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"

View File

@@ -30,6 +30,7 @@ from .const import (
DOMAIN,
SWITCH_TYPE_DEFLECTION,
SWITCH_TYPE_PORTFORWARD,
SWITCH_TYPE_PROFILE,
SWITCH_TYPE_WIFINETWORK,
WIFI_STANDARD,
MeshRoles,
@@ -185,6 +186,7 @@ def profile_entities_list(
data_fritz: FritzData,
) -> list[FritzBoxProfileSwitch]:
"""Add new tracker entities from the AVM device."""
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PROFILE)
new_profiles: list[FritzBoxProfileSwitch] = []
@@ -198,11 +200,15 @@ def profile_entities_list(
if device_filter_out_from_trackers(
mac, device, data_fritz.profile_switches.values()
):
_LOGGER.debug(
"Skipping profile switch creation for device %s", device.hostname
)
continue
new_profiles.append(FritzBoxProfileSwitch(avm_wrapper, device))
data_fritz.profile_switches[avm_wrapper.unique_id].add(mac)
_LOGGER.debug("Creating %s profile switches", len(new_profiles))
return new_profiles

View File

@@ -9,7 +9,8 @@
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
"invalid_auth": "Invalid authentication",
"upnp_not_configured": "Missing UPnP settings on device."
},
"flow_title": "{name}",
"step": {
@@ -51,4 +52,4 @@
}
}
}
}
}

View File

@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20220301.0"
"home-assistant-frontend==20220301.1"
],
"dependencies": [
"api",
@@ -13,7 +13,8 @@
"diagnostics",
"http",
"lovelace",
"onboarding", "search",
"onboarding",
"search",
"system_log",
"websocket_api"
],

View File

@@ -59,11 +59,12 @@ SERVICE_SET = "set"
SERVICE_REMOVE = "remove"
PLATFORMS = [
Platform.LIGHT,
Platform.COVER,
Platform.NOTIFY,
Platform.FAN,
Platform.BINARY_SENSOR,
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.MEDIA_PLAYER,
Platform.NOTIFY,
]
REG_KEY = f"{DOMAIN}_registry"

View File

@@ -221,12 +221,9 @@ class GrowattData:
# Create datetime from the latest entry
date_now = dt.now().date()
last_updated_time = dt.parse_time(str(sorted_keys[-1]))
combined_timestamp = datetime.datetime.combine(
date_now, last_updated_time
mix_detail["lastdataupdate"] = datetime.datetime.combine(
date_now, last_updated_time, dt.DEFAULT_TIME_ZONE
)
# Convert datetime to UTC
combined_timestamp_utc = dt.as_utc(combined_timestamp)
mix_detail["lastdataupdate"] = combined_timestamp_utc.isoformat()
# Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined
# imported from grid value that is the combination of charging AND load consumption

View File

@@ -274,7 +274,7 @@ class HomeAccessory(Accessory):
if self.config.get(ATTR_SW_VERSION) is not None:
sw_version = format_version(self.config[ATTR_SW_VERSION])
if sw_version is None:
sw_version = __version__
sw_version = format_version(__version__)
hw_version = None
if self.config.get(ATTR_HW_VERSION) is not None:
hw_version = format_version(self.config[ATTR_HW_VERSION])
@@ -289,7 +289,9 @@ class HomeAccessory(Accessory):
serv_info = self.get_service(SERV_ACCESSORY_INFO)
char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION)
serv_info.add_characteristic(char)
serv_info.configure_char(CHAR_HARDWARE_REVISION, value=hw_version)
serv_info.configure_char(
CHAR_HARDWARE_REVISION, value=hw_version[:MAX_VERSION_LENGTH]
)
self.iid_manager.assign(char)
char.broker = self
@@ -532,7 +534,7 @@ class HomeBridge(Bridge):
"""Initialize a Bridge object."""
super().__init__(driver, name)
self.set_info_service(
firmware_revision=__version__,
firmware_revision=format_version(__version__),
manufacturer=MANUFACTURER,
model=BRIDGE_MODEL,
serial_number=BRIDGE_SERIAL_NUMBER,

View File

@@ -285,20 +285,19 @@ class Thermostat(HomeAccessory):
CHAR_CURRENT_HUMIDITY, value=50
)
fan_modes = self.fan_modes = {
fan_mode.lower(): fan_mode
for fan_mode in attributes.get(ATTR_FAN_MODES, [])
}
fan_modes = {}
self.ordered_fan_speeds = []
if (
features & SUPPORT_FAN_MODE
and fan_modes
and PRE_DEFINED_FAN_MODES.intersection(fan_modes)
):
self.ordered_fan_speeds = [
speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes
]
self.fan_chars.append(CHAR_ROTATION_SPEED)
if features & SUPPORT_FAN_MODE:
fan_modes = {
fan_mode.lower(): fan_mode
for fan_mode in attributes.get(ATTR_FAN_MODES) or []
}
if fan_modes and PRE_DEFINED_FAN_MODES.intersection(fan_modes):
self.ordered_fan_speeds = [
speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes
]
self.fan_chars.append(CHAR_ROTATION_SPEED)
if FAN_AUTO in fan_modes and (FAN_ON in fan_modes or self.ordered_fan_speeds):
self.fan_chars.append(CHAR_TARGET_FAN_STATE)

View File

@@ -100,6 +100,7 @@ _LOGGER = logging.getLogger(__name__)
NUMBERS_ONLY_RE = re.compile(r"[^\d.]+")
VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?")
MAX_VERSION_PART = 2**32 - 1
MAX_PORT = 65535
@@ -363,7 +364,15 @@ def convert_to_float(state):
return None
def cleanup_name_for_homekit(name: str | None) -> str | None:
def coerce_int(state: str) -> int:
"""Return int."""
try:
return int(state)
except (ValueError, TypeError):
return 0
def cleanup_name_for_homekit(name: str | None) -> str:
"""Ensure the name of the device will not crash homekit."""
#
# This is not a security measure.
@@ -371,7 +380,7 @@ def cleanup_name_for_homekit(name: str | None) -> str | None:
# UNICODE_EMOJI is also not allowed but that
# likely isn't a problem
if name is None:
return None
return "None" # None crashes apple watches
return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH]
@@ -420,13 +429,23 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str):
)
def _format_version_part(version_part: str) -> str:
return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part))))
def format_version(version):
"""Extract the version string in a format homekit can consume."""
split_ver = str(version).replace("-", ".")
split_ver = str(version).replace("-", ".").replace(" ", ".")
num_only = NUMBERS_ONLY_RE.sub("", split_ver)
if match := VERSION_RE.search(num_only):
return match.group(0)
return None
if (match := VERSION_RE.search(num_only)) is None:
return None
value = ".".join(map(_format_version_part, match.group(0).split(".")))
return None if _is_zero_but_true(value) else value
def _is_zero_but_true(value):
"""Zero but true values can crash apple watches."""
return convert_to_float(value) == 0
def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str):

View File

@@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==0.7.15"],
"requirements": ["aiohomekit==0.7.16"],
"zeroconf": ["_hap._tcp.local."],
"after_dependencies": ["zeroconf"],
"codeowners": ["@Jc2k", "@bdraco"],

View File

@@ -3,7 +3,7 @@
"name": "LIFX",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lifx",
"requirements": ["aiolifx==0.7.0", "aiolifx_effects==0.2.2"],
"requirements": ["aiolifx==0.7.1", "aiolifx_effects==0.2.2"],
"homekit": {
"models": ["LIFX"]
},

View File

@@ -75,11 +75,16 @@ from .const import (
ATTR_TOPIC,
CONF_BIRTH_MESSAGE,
CONF_BROKER,
CONF_CERTIFICATE,
CONF_CLIENT_CERT,
CONF_CLIENT_KEY,
CONF_COMMAND_TOPIC,
CONF_ENCODING,
CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC,
CONF_TLS_INSECURE,
CONF_TLS_VERSION,
CONF_TOPIC,
CONF_WILL_MESSAGE,
DATA_MQTT_CONFIG,
@@ -94,6 +99,7 @@ from .const import (
DOMAIN,
MQTT_CONNECTED,
MQTT_DISCONNECTED,
PROTOCOL_31,
PROTOCOL_311,
)
from .discovery import LAST_DISCOVERY
@@ -118,13 +124,6 @@ SERVICE_DUMP = "dump"
CONF_DISCOVERY_PREFIX = "discovery_prefix"
CONF_KEEPALIVE = "keepalive"
CONF_CERTIFICATE = "certificate"
CONF_CLIENT_KEY = "client_key"
CONF_CLIENT_CERT = "client_cert"
CONF_TLS_INSECURE = "tls_insecure"
CONF_TLS_VERSION = "tls_version"
PROTOCOL_31 = "3.1"
DEFAULT_PORT = 1883
DEFAULT_KEEPALIVE = 60
@@ -757,6 +756,58 @@ class Subscription:
encoding: str | None = attr.ib(default="utf-8")
class MqttClientSetup:
"""Helper class to setup the paho mqtt client from config."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
def __init__(self, config: ConfigType) -> None:
"""Initialize the MQTT client setup helper."""
if config[CONF_PROTOCOL] == PROTOCOL_31:
proto = self.mqtt.MQTTv31
else:
proto = self.mqtt.MQTTv311
if (client_id := config.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = self.mqtt.base62(uuid.uuid4().int, padding=22)
self._client = self.mqtt.Client(client_id, protocol=proto)
# Enable logging
self._client.enable_logger()
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if username is not None:
self._client.username_pw_set(username, password)
if (certificate := config.get(CONF_CERTIFICATE)) == "auto":
certificate = certifi.where()
client_key = config.get(CONF_CLIENT_KEY)
client_cert = config.get(CONF_CLIENT_CERT)
tls_insecure = config.get(CONF_TLS_INSECURE)
if certificate is not None:
self._client.tls_set(
certificate,
certfile=client_cert,
keyfile=client_key,
tls_version=ssl.PROTOCOL_TLS,
)
if tls_insecure is not None:
self._client.tls_insecure_set(tls_insecure)
@property
def client(self) -> mqtt.Client:
"""Return the paho MQTT client."""
return self._client
class MQTT:
"""Home Assistant MQTT client."""
@@ -821,46 +872,7 @@ class MQTT:
def init_client(self):
"""Initialize paho client."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
if self.conf[CONF_PROTOCOL] == PROTOCOL_31:
proto: int = mqtt.MQTTv31
else:
proto = mqtt.MQTTv311
if (client_id := self.conf.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = mqtt.base62(uuid.uuid4().int, padding=22)
self._mqttc = mqtt.Client(client_id, protocol=proto)
# Enable logging
self._mqttc.enable_logger()
username = self.conf.get(CONF_USERNAME)
password = self.conf.get(CONF_PASSWORD)
if username is not None:
self._mqttc.username_pw_set(username, password)
if (certificate := self.conf.get(CONF_CERTIFICATE)) == "auto":
certificate = certifi.where()
client_key = self.conf.get(CONF_CLIENT_KEY)
client_cert = self.conf.get(CONF_CLIENT_CERT)
tls_insecure = self.conf.get(CONF_TLS_INSECURE)
if certificate is not None:
self._mqttc.tls_set(
certificate,
certfile=client_cert,
keyfile=client_key,
tls_version=ssl.PROTOCOL_TLS,
)
if tls_insecure is not None:
self._mqttc.tls_insecure_set(tls_insecure)
self._mqttc = MqttClientSetup(self.conf).client
self._mqttc.on_connect = self._mqtt_on_connect
self._mqttc.on_disconnect = self._mqtt_on_disconnect
self._mqttc.on_message = self._mqtt_on_message

View File

@@ -271,7 +271,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend(
vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list,
vol.Optional(CONF_HOLD_LIST): cv.ensure_list,
vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(
@@ -298,7 +298,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend(
),
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
vol.Optional(CONF_SEND_IF_OFF): cv.boolean,
vol.Optional(CONF_ACTION_TEMPLATE): cv.template,
vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic,
# CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together
@@ -431,6 +431,12 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._feature_preset_mode = False
self._optimistic_preset_mode = None
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
self._send_if_off = True
# AWAY and HOLD mode topics and templates are deprecated,
# support will be removed with release 2022.9
self._hold_list = []
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@staticmethod
@@ -499,6 +505,15 @@ class MqttClimate(MqttEntity, ClimateEntity):
self._command_templates = command_templates
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if CONF_SEND_IF_OFF in config:
self._send_if_off = config[CONF_SEND_IF_OFF]
# AWAY and HOLD mode topics and templates are deprecated,
# support will be removed with release 2022.9
if CONF_HOLD_LIST in config:
self._hold_list = config[CONF_HOLD_LIST]
def _prepare_subscribe_topics(self): # noqa: C901
"""(Re)Subscribe to topics."""
topics = {}
@@ -806,7 +821,9 @@ class MqttClimate(MqttEntity, ClimateEntity):
):
presets.append(PRESET_AWAY)
presets.extend(self._config[CONF_HOLD_LIST])
# AWAY and HOLD mode topics and templates are deprecated,
# support will be removed with release 2022.9
presets.extend(self._hold_list)
if presets:
presets.insert(0, PRESET_NONE)
@@ -847,10 +864,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
setattr(self, attr, temp)
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if (
self._config[CONF_SEND_IF_OFF]
or self._current_operation != HVAC_MODE_OFF
):
if self._send_if_off or self._current_operation != HVAC_MODE_OFF:
payload = self._command_templates[cmnd_template](temp)
await self._publish(cmnd_topic, payload)
@@ -890,7 +904,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
async def async_set_swing_mode(self, swing_mode):
"""Set new swing mode."""
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF:
if self._send_if_off or self._current_operation != HVAC_MODE_OFF:
payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](
swing_mode
)
@@ -903,7 +917,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
async def async_set_fan_mode(self, fan_mode):
"""Set new target temperature."""
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF:
if self._send_if_off or self._current_operation != HVAC_MODE_OFF:
payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode)
await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload)

View File

@@ -17,6 +17,7 @@ from homeassistant.const import (
)
from homeassistant.data_entry_flow import FlowResult
from . import MqttClientSetup
from .const import (
ATTR_PAYLOAD,
ATTR_QOS,
@@ -62,6 +63,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
can_connect = await self.hass.async_add_executor_job(
try_connection,
self.hass,
user_input[CONF_BROKER],
user_input[CONF_PORT],
user_input.get(CONF_USERNAME),
@@ -102,6 +104,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data = self._hassio_discovery
can_connect = await self.hass.async_add_executor_job(
try_connection,
self.hass,
data[CONF_HOST],
data[CONF_PORT],
data.get(CONF_USERNAME),
@@ -152,6 +155,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
if user_input is not None:
can_connect = await self.hass.async_add_executor_job(
try_connection,
self.hass,
user_input[CONF_BROKER],
user_input[CONF_PORT],
user_input.get(CONF_USERNAME),
@@ -313,25 +317,24 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
)
def try_connection(broker, port, username, password, protocol="3.1"):
def try_connection(hass, broker, port, username, password, protocol="3.1"):
"""Test if we can connect to an MQTT broker."""
# pylint: disable-next=import-outside-toplevel
import paho.mqtt.client as mqtt
if protocol == "3.1":
proto = mqtt.MQTTv31
else:
proto = mqtt.MQTTv311
client = mqtt.Client(protocol=proto)
if username and password:
client.username_pw_set(username, password)
# Get the config from configuration.yaml
yaml_config = hass.data.get(DATA_MQTT_CONFIG, {})
entry_config = {
CONF_BROKER: broker,
CONF_PORT: port,
CONF_USERNAME: username,
CONF_PASSWORD: password,
CONF_PROTOCOL: protocol,
}
client = MqttClientSetup({**yaml_config, **entry_config}).client
result = queue.Queue(maxsize=1)
def on_connect(client_, userdata, flags, result_code):
"""Handle connection result."""
result.put(result_code == mqtt.CONNACK_ACCEPTED)
result.put(result_code == MqttClientSetup.mqtt.CONNACK_ACCEPTED)
client.on_connect = on_connect

View File

@@ -22,6 +22,12 @@ CONF_STATE_VALUE_TEMPLATE = "state_value_template"
CONF_TOPIC = "topic"
CONF_WILL_MESSAGE = "will_message"
CONF_CERTIFICATE = "certificate"
CONF_CLIENT_KEY = "client_key"
CONF_CLIENT_CERT = "client_cert"
CONF_TLS_INSECURE = "tls_insecure"
CONF_TLS_VERSION = "tls_version"
DATA_MQTT_CONFIG = "mqtt_config"
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"
@@ -56,4 +62,5 @@ MQTT_DISCONNECTED = "mqtt_disconnected"
PAYLOAD_EMPTY_JSON = "{}"
PAYLOAD_NONE = "None"
PROTOCOL_31 = "3.1"
PROTOCOL_311 = "3.1.1"

View File

@@ -2,7 +2,7 @@
"domain": "obihai",
"name": "Obihai",
"documentation": "https://www.home-assistant.io/integrations/obihai",
"requirements": ["pyobihai==1.3.1"],
"requirements": ["pyobihai==1.3.2"],
"codeowners": ["@dshokouhi"],
"iot_class": "local_polling",
"loggers": ["pyobihai"]

View File

@@ -104,7 +104,7 @@ class RenaultVehicleProxy:
coordinator = self.coordinators[key]
if coordinator.not_supported:
# Remove endpoint as it is not supported for this vehicle.
LOGGER.warning(
LOGGER.info(
"Ignoring endpoint %s as it is not supported for this vehicle: %s",
coordinator.name,
coordinator.last_exception,
@@ -112,7 +112,7 @@ class RenaultVehicleProxy:
del self.coordinators[key]
elif coordinator.access_denied:
# Remove endpoint as it is denied for this vehicle.
LOGGER.warning(
LOGGER.info(
"Ignoring endpoint %s as it is denied for this vehicle: %s",
coordinator.name,
coordinator.last_exception,

View File

@@ -2,7 +2,7 @@
"domain": "rfxtrx",
"name": "RFXCOM RFXtrx",
"documentation": "https://www.home-assistant.io/integrations/rfxtrx",
"requirements": ["pyRFXtrx==0.27.1"],
"requirements": ["pyRFXtrx==0.28.0"],
"codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"],
"config_flow": true,
"iot_class": "local_push",

View File

@@ -1,14 +1,6 @@
"""Support for Roku."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, TypeVar
from rokuecp import RokuConnectionError, RokuError
from typing_extensions import Concatenate, ParamSpec
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
@@ -16,7 +8,6 @@ from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@@ -27,10 +18,6 @@ PLATFORMS = [
Platform.SELECT,
Platform.SENSOR,
]
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T", bound="RokuEntity")
_P = ParamSpec("_P")
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -53,22 +40,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
def roku_exception_handler(
func: Callable[Concatenate[_T, _P], Awaitable[None]] # type: ignore[misc]
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
"""Decorate Roku calls to handle Roku exceptions."""
@wraps(func)
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
except RokuConnectionError as error:
if self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuError as error:
if self.available:
_LOGGER.error("Invalid response from API: %s", error)
return wrapper

View File

@@ -1,6 +1,21 @@
"""Helpers for Roku."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, TypeVar
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
from typing_extensions import Concatenate, ParamSpec
from .entity import RokuEntity
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T", bound=RokuEntity)
_P = ParamSpec("_P")
def format_channel_name(channel_number: str, channel_name: str | None = None) -> str:
"""Format a Roku Channel name."""
@@ -8,3 +23,28 @@ def format_channel_name(channel_number: str, channel_name: str | None = None) ->
return f"{channel_name} ({channel_number})"
return channel_number
def roku_exception_handler(ignore_timeout: bool = False) -> Callable[..., Callable]:
"""Decorate Roku calls to handle Roku exceptions."""
def decorator(
func: Callable[Concatenate[_T, _P], Awaitable[None]], # type: ignore[misc]
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
@wraps(func)
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
except RokuConnectionTimeoutError as error:
if not ignore_timeout and self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuConnectionError as error:
if self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuError as error:
if self.available:
_LOGGER.error("Invalid response from API: %s", error)
return wrapper
return decorator

View File

@@ -2,7 +2,7 @@
"domain": "roku",
"name": "Roku",
"documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["rokuecp==0.14.1"],
"requirements": ["rokuecp==0.15.0"],
"homekit": {
"models": ["3810X", "4660X", "7820X", "C105X", "C135X"]
},

View File

@@ -51,7 +51,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .browse_media import async_browse_media
from .const import (
ATTR_ARTIST_NAME,
@@ -65,7 +64,7 @@ from .const import (
)
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import format_channel_name
from .helpers import format_channel_name, roku_exception_handler
_LOGGER = logging.getLogger(__name__)
@@ -289,7 +288,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
app.name for app in self.coordinator.data.apps if app.name is not None
)
@roku_exception_handler
@roku_exception_handler()
async def search(self, keyword: str) -> None:
"""Emulate opening the search screen and entering the search keyword."""
await self.coordinator.roku.search(keyword)
@@ -321,68 +320,68 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
media_content_type,
)
@roku_exception_handler
@roku_exception_handler()
async def async_turn_on(self) -> None:
"""Turn on the Roku."""
await self.coordinator.roku.remote("poweron")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler(ignore_timeout=True)
async def async_turn_off(self) -> None:
"""Turn off the Roku."""
await self.coordinator.roku.remote("poweroff")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_pause(self) -> None:
"""Send pause command."""
if self.state not in (STATE_STANDBY, STATE_PAUSED):
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_play(self) -> None:
"""Send play command."""
if self.state not in (STATE_STANDBY, STATE_PLAYING):
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_play_pause(self) -> None:
"""Send play/pause command."""
if self.state != STATE_STANDBY:
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self.coordinator.roku.remote("reverse")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self.coordinator.roku.remote("forward")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
await self.coordinator.roku.remote("volume_mute")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_volume_up(self) -> None:
"""Volume up media player."""
await self.coordinator.roku.remote("volume_up")
@roku_exception_handler
@roku_exception_handler()
async def async_volume_down(self) -> None:
"""Volume down media player."""
await self.coordinator.roku.remote("volume_down")
@roku_exception_handler
@roku_exception_handler()
async def async_play_media(
self, media_type: str, media_id: str, **kwargs: Any
) -> None:
@@ -487,7 +486,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_select_source(self, source: str) -> None:
"""Select input source."""
if source == "Home":

View File

@@ -9,10 +9,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import roku_exception_handler
async def async_setup_entry(
@@ -44,19 +44,19 @@ class RokuRemote(RokuEntity, RemoteEntity):
"""Return true if device is on."""
return not self.coordinator.data.state.standby
@roku_exception_handler
@roku_exception_handler()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self.coordinator.roku.remote("poweron")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler(ignore_timeout=True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self.coordinator.roku.remote("poweroff")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to one device."""
num_repeats = kwargs[ATTR_NUM_REPEATS]

View File

@@ -12,11 +12,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import format_channel_name
from .helpers import format_channel_name, roku_exception_handler
@dataclass
@@ -163,7 +162,7 @@ class RokuSelectEntity(RokuEntity, SelectEntity):
"""Return a set of selectable options."""
return self.entity_description.options_fn(self.coordinator.data)
@roku_exception_handler
@roku_exception_handler()
async def async_select_option(self, option: str) -> None:
"""Set the option."""
await self.entity_description.set_fn(

View File

@@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components.light import ATTR_TRANSITION
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON
from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON, STATE_UNAVAILABLE
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
@@ -117,7 +117,11 @@ class Scene(RestoreEntity):
"""Call when the scene is added to hass."""
await super().async_internal_added_to_hass()
state = await self.async_get_last_state()
if state is not None and state.state is not None:
if (
state is not None
and state.state is not None
and state.state != STATE_UNAVAILABLE
):
self.__last_activated = state.state
def activate(self, **kwargs: Any) -> None:

View File

@@ -15,6 +15,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT
MAX_POSSIBLE_STEP = 1000
class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
"""A Sensibo Data Update Coordinator."""
@@ -74,7 +76,11 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
.get("values", [0, 1])
)
if temperatures_list:
temperature_step = temperatures_list[1] - temperatures_list[0]
diff = MAX_POSSIBLE_STEP
for i in range(len(temperatures_list) - 1):
if temperatures_list[i + 1] - temperatures_list[i] < diff:
diff = temperatures_list[i + 1] - temperatures_list[i]
temperature_step = diff
active_features = list(ac_states)
full_features = set()

View File

@@ -317,4 +317,14 @@ class BlockSleepingClimate(
if self.device_block and self.block:
_LOGGER.debug("Entity %s attached to blocks", self.name)
assert self.block.channel
self._preset_modes = [
PRESET_NONE,
*self.wrapper.device.settings["thermostats"][int(self.block.channel)][
"schedule_profile_names"
],
]
self.async_write_ha_state()

View File

@@ -3,7 +3,7 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.26.3"],
"requirements": ["soco==0.26.4"],
"dependencies": ["ssdp"],
"after_dependencies": ["plex", "spotify", "zeroconf", "media_source"],
"zeroconf": ["_sonos._tcp.local."],

View File

@@ -172,7 +172,7 @@ class SQLSensor(SensorEntity):
else:
self._attr_native_value = data
if not data:
if data is None:
_LOGGER.warning("%s returned no results", self._query)
sess.close()

View File

@@ -277,8 +277,6 @@ class TemplateFan(TemplateEntity, FanEntity):
"""Turn off the fan."""
await self._off_script.async_run(context=self._context)
self._state = STATE_OFF
self._percentage = 0
self._preset_mode = None
async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage speed of the fan."""

View File

@@ -14,7 +14,6 @@ from miio import (
AirHumidifierMiot,
AirHumidifierMjjsq,
AirPurifier,
AirPurifierMB4,
AirPurifierMiot,
CleaningDetails,
CleaningSummary,
@@ -23,10 +22,8 @@ from miio import (
DNDStatus,
Fan,
Fan1C,
FanMiot,
FanP5,
FanP9,
FanP10,
FanP11,
FanZA5,
RoborockVacuum,
Timer,
@@ -52,7 +49,6 @@ from .const import (
KEY_DEVICE,
MODEL_AIRFRESH_A1,
MODEL_AIRFRESH_T2017,
MODEL_AIRPURIFIER_3C,
MODEL_FAN_1C,
MODEL_FAN_P5,
MODEL_FAN_P9,
@@ -111,10 +107,10 @@ AIR_MONITOR_PLATFORMS = [Platform.AIR_QUALITY, Platform.SENSOR]
MODEL_TO_CLASS_MAP = {
MODEL_FAN_1C: Fan1C,
MODEL_FAN_P10: FanP10,
MODEL_FAN_P11: FanP11,
MODEL_FAN_P9: FanMiot,
MODEL_FAN_P10: FanMiot,
MODEL_FAN_P11: FanMiot,
MODEL_FAN_P5: FanP5,
MODEL_FAN_P9: FanP9,
MODEL_FAN_ZA5: FanZA5,
}
@@ -314,8 +310,6 @@ async def async_create_miio_device_and_coordinator(
device = AirHumidifier(host, token, model=model)
migrate = True
# Airpurifiers and Airfresh
elif model == MODEL_AIRPURIFIER_3C:
device = AirPurifierMB4(host, token)
elif model in MODELS_PURIFIER_MIOT:
device = AirPurifierMiot(host, token)
elif model.startswith("zhimi.airpurifier."):

View File

@@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.10"],
"requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.11"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
"zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling",

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "0"
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, 9, 0)

View File

@@ -6,6 +6,7 @@ import asyncio
from collections.abc import Awaitable, Iterable, Mapping, MutableMapping
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum, auto
import functools as ft
import logging
import math
@@ -207,6 +208,19 @@ class EntityCategory(StrEnum):
SYSTEM = "system"
class EntityPlatformState(Enum):
"""The platform state of an entity."""
# Not Added: Not yet added to a platform, polling updates are written to the state machine
NOT_ADDED = auto()
# Added: Added to a platform, polling updates are written to the state machine
ADDED = auto()
# Removed: Removed from a platform, polling updates are not written to the state machine
REMOVED = auto()
def convert_to_entity_category(
value: EntityCategory | str | None, raise_report: bool = True
) -> EntityCategory | None:
@@ -294,7 +308,7 @@ class Entity(ABC):
_context_set: datetime | None = None
# If entity is added to an entity platform
_added = False
_platform_state = EntityPlatformState.NOT_ADDED
# Entity Properties
_attr_assumed_state: bool = False
@@ -553,6 +567,10 @@ class Entity(ABC):
@callback
def _async_write_ha_state(self) -> None:
"""Write the state to the state machine."""
if self._platform_state == EntityPlatformState.REMOVED:
# Polling returned after the entity has already been removed
return
if self.registry_entry and self.registry_entry.disabled_by:
if not self._disabled_reported:
self._disabled_reported = True
@@ -758,7 +776,7 @@ class Entity(ABC):
parallel_updates: asyncio.Semaphore | None,
) -> None:
"""Start adding an entity to a platform."""
if self._added:
if self._platform_state == EntityPlatformState.ADDED:
raise HomeAssistantError(
f"Entity {self.entity_id} cannot be added a second time to an entity platform"
)
@@ -766,7 +784,7 @@ class Entity(ABC):
self.hass = hass
self.platform = platform
self.parallel_updates = parallel_updates
self._added = True
self._platform_state = EntityPlatformState.ADDED
@callback
def add_to_platform_abort(self) -> None:
@@ -774,7 +792,7 @@ class Entity(ABC):
self.hass = None # type: ignore[assignment]
self.platform = None
self.parallel_updates = None
self._added = False
self._platform_state = EntityPlatformState.NOT_ADDED
async def add_to_platform_finish(self) -> None:
"""Finish adding an entity to a platform."""
@@ -792,12 +810,12 @@ class Entity(ABC):
If the entity doesn't have a non disabled entry in the entity registry,
or if force_remove=True, its state will be removed.
"""
if self.platform and not self._added:
if self.platform and self._platform_state != EntityPlatformState.ADDED:
raise HomeAssistantError(
f"Entity {self.entity_id} async_remove called twice"
)
self._added = False
self._platform_state = EntityPlatformState.REMOVED
if self._on_remove is not None:
while self._on_remove:

View File

@@ -14,7 +14,7 @@ certifi>=2021.5.30
ciso8601==2.2.0
cryptography==35.0.0
hass-nabucasa==0.54.0
home-assistant-frontend==20220301.0
home-assistant-frontend==20220301.1
httpx==0.21.3
ifaddr==0.1.7
jinja2==3.0.3

View File

@@ -149,10 +149,17 @@ async def _async_setup_component(
This method is a coroutine.
"""
integration: loader.Integration | None = None
def log_error(msg: str, link: str | None = None) -> None:
def log_error(msg: str) -> None:
"""Log helper."""
_LOGGER.error("Setup failed for %s: %s", domain, msg)
if integration is None:
custom = ""
link = None
else:
custom = "" if integration.is_built_in else "custom integration "
link = integration.documentation
_LOGGER.error("Setup failed for %s%s: %s", custom, domain, msg)
async_notify_setup_error(hass, domain, link)
try:
@@ -174,7 +181,7 @@ async def _async_setup_component(
try:
await async_process_deps_reqs(hass, config, integration)
except HomeAssistantError as err:
log_error(str(err), integration.documentation)
log_error(str(err))
return False
# Some integrations fail on import because they call functions incorrectly.
@@ -182,7 +189,7 @@ async def _async_setup_component(
try:
component = integration.get_component()
except ImportError as err:
log_error(f"Unable to import component: {err}", integration.documentation)
log_error(f"Unable to import component: {err}")
return False
processed_config = await conf_util.async_process_component_config(
@@ -190,7 +197,7 @@ async def _async_setup_component(
)
if processed_config is None:
log_error("Invalid config.", integration.documentation)
log_error("Invalid config.")
return False
start = timer()
@@ -287,6 +294,7 @@ async def async_prepare_setup_platform(
def log_error(msg: str) -> None:
"""Log helper."""
_LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg)
async_notify_setup_error(hass, platform_path)

View File

@@ -184,7 +184,7 @@ aioguardian==2021.11.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==0.7.15
aiohomekit==0.7.16
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -206,7 +206,7 @@ aiokafka==0.6.0
aiokef==0.2.16
# homeassistant.components.lifx
aiolifx==0.7.0
aiolifx==0.7.1
# homeassistant.components.lifx
aiolifx_effects==0.2.2
@@ -843,7 +843,7 @@ hole==0.7.0
holidays==0.13
# homeassistant.components.frontend
home-assistant-frontend==20220301.0
home-assistant-frontend==20220301.1
# homeassistant.components.zwave
# homeassistant-pyozw==0.1.10
@@ -1360,7 +1360,7 @@ pyMetEireann==2021.8.0
pyMetno==0.9.0
# homeassistant.components.rfxtrx
pyRFXtrx==0.27.1
pyRFXtrx==0.28.0
# homeassistant.components.switchmate
# pySwitchmate==0.4.6
@@ -1486,7 +1486,7 @@ pydispatcher==2.0.5
pydoods==1.0.2
# homeassistant.components.android_ip_webcam
pydroid-ipcam==0.8
pydroid-ipcam==1.3.1
# homeassistant.components.ebox
pyebox==1.1.4
@@ -1727,7 +1727,7 @@ pynx584==0.5
pynzbgetapi==0.2.0
# homeassistant.components.obihai
pyobihai==1.3.1
pyobihai==1.3.2
# homeassistant.components.octoprint
pyoctoprintapi==0.1.7
@@ -1952,7 +1952,7 @@ python-kasa==0.4.1
# python-lirc==1.2.3
# homeassistant.components.xiaomi_miio
python-miio==0.5.10
python-miio==0.5.11
# homeassistant.components.mpd
python-mpd2==3.0.4
@@ -2121,7 +2121,7 @@ rjpl==0.3.6
rocketchat-API==0.6.1
# homeassistant.components.roku
rokuecp==0.14.1
rokuecp==0.15.0
# homeassistant.components.roomba
roombapy==1.6.5
@@ -2235,7 +2235,7 @@ smhi-pkg==1.0.15
snapcast==2.1.3
# homeassistant.components.sonos
soco==0.26.3
soco==0.26.4
# homeassistant.components.solaredge_local
solaredge-local==0.2.0

View File

@@ -134,7 +134,7 @@ aioguardian==2021.11.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==0.7.15
aiohomekit==0.7.16
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -553,7 +553,7 @@ hole==0.7.0
holidays==0.13
# homeassistant.components.frontend
home-assistant-frontend==20220301.0
home-assistant-frontend==20220301.1
# homeassistant.components.zwave
# homeassistant-pyozw==0.1.10
@@ -858,7 +858,7 @@ pyMetEireann==2021.8.0
pyMetno==0.9.0
# homeassistant.components.rfxtrx
pyRFXtrx==0.27.1
pyRFXtrx==0.28.0
# homeassistant.components.tibber
pyTibber==0.22.1
@@ -1219,7 +1219,7 @@ python-juicenet==1.0.2
python-kasa==0.4.1
# homeassistant.components.xiaomi_miio
python-miio==0.5.10
python-miio==0.5.11
# homeassistant.components.nest
python-nest==4.2.0
@@ -1313,7 +1313,7 @@ rflink==0.0.62
ring_doorbell==0.7.2
# homeassistant.components.roku
rokuecp==0.14.1
rokuecp==0.15.0
# homeassistant.components.roomba
roombapy==1.6.5
@@ -1371,7 +1371,7 @@ smarthab==0.21
smhi-pkg==1.0.15
# homeassistant.components.sonos
soco==0.26.3
soco==0.26.4
# homeassistant.components.solaredge
solaredge==0.0.2

View File

@@ -1,6 +1,6 @@
[metadata]
name = homeassistant
version = 2022.3.0
version = 2022.3.3
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -73,6 +73,155 @@ async def test_form_user_with_secure_elk_no_discovery(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_with_insecure_elk_skip_discovery(hass):
"""Test we can setup a insecure elk with skipping discovery."""
with _patch_discovery(), _patch_elk():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
)
await hass.async_block_till_done()
with _patch_discovery(no_device=True):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "manual_connection"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
with _patch_discovery(), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"protocol": "non-secure",
"address": "1.2.3.4",
"username": "test-username",
"password": "test-password",
"prefix": "",
},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "ElkM1"
assert result2["data"] == {
"auto_configure": True,
"host": "elk://1.2.3.4",
"password": "test-password",
"prefix": "",
"username": "test-username",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_with_insecure_elk_no_discovery(hass):
"""Test we can setup a insecure elk."""
with _patch_discovery(), _patch_elk():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
)
await hass.async_block_till_done()
with _patch_discovery(no_device=True):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "manual_connection"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"protocol": "non-secure",
"address": "1.2.3.4",
"username": "test-username",
"password": "test-password",
"prefix": "",
},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "ElkM1"
assert result2["data"] == {
"auto_configure": True,
"host": "elk://1.2.3.4",
"password": "test-password",
"prefix": "",
"username": "test-username",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_with_insecure_elk_times_out(hass):
"""Test we can setup a insecure elk that times out."""
with _patch_discovery(), _patch_elk():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
)
await hass.async_block_till_done()
with _patch_discovery(no_device=True):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "manual_connection"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=False)
with patch(
"homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT",
0,
), patch(
"homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0
), _patch_discovery(), _patch_elk(
elk=mocked_elk
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"protocol": "non-secure",
"address": "1.2.3.4",
"username": "test-username",
"password": "test-password",
"prefix": "",
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_user_with_secure_elk_no_discovery_ip_already_configured(hass):
"""Test we abort when we try to configure the same ip."""
config_entry = MockConfigEntry(
@@ -262,7 +411,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco
assert result3["title"] == "ElkM1 ddeeff"
assert result3["data"] == {
"auto_configure": True,
"host": "elks://127.0.0.1:2601",
"host": "elks://127.0.0.1",
"password": "test-password",
"prefix": "",
"username": "test-username",
@@ -434,7 +583,7 @@ async def test_form_cannot_connect(hass):
)
assert result2["type"] == "form"
assert result2["errors"] == {CONF_HOST: "cannot_connect"}
assert result2["errors"] == {"base": "cannot_connect"}
async def test_unknown_exception(hass):

View File

@@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .const import MOCK_USER_DATA
from .const import MOCK_MESH_MASTER_MAC, MOCK_USER_DATA
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
@@ -69,6 +69,7 @@ async def test_entry_diagnostics(
"latest_firmware": None,
"mesh_role": "master",
"model": "FRITZ!Box 7530 AX",
"unique_id": MOCK_MESH_MASTER_MAC.replace("6F:12", "XX:XX"),
"update_available": False,
"wan_link_properties": {
"NewLayer1DownstreamMaxBitRate": 318557000,

View File

@@ -42,7 +42,6 @@ from homeassistant.const import (
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
__version__,
__version__ as hass_version,
)
from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS
@@ -166,7 +165,9 @@ async def test_home_accessory(hass, hk_driver):
serv.get_characteristic(CHAR_SERIAL_NUMBER).value
== "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum"
)
assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version
assert hass_version.startswith(
serv.get_characteristic(CHAR_FIRMWARE_REVISION).value
)
hass.states.async_set(entity_id, "on")
await hass.async_block_till_done()
@@ -216,7 +217,9 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver):
assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor"
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id
assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version
assert hass_version.startswith(
serv.get_characteristic(CHAR_FIRMWARE_REVISION).value
)
assert isinstance(acc.to_HAP(), dict)
@@ -244,7 +247,9 @@ async def test_accessory_with_hardware_revision(hass, hk_driver):
assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor"
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id
assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version
assert hass_version.startswith(
serv.get_characteristic(CHAR_FIRMWARE_REVISION).value
)
assert serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3"
assert isinstance(acc.to_HAP(), dict)
@@ -687,7 +692,9 @@ def test_home_bridge(hk_driver):
serv = bridge.services[0] # SERV_ACCESSORY_INFO
assert serv.display_name == SERV_ACCESSORY_INFO
assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME
assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == __version__
assert hass_version.startswith(
serv.get_characteristic(CHAR_FIRMWARE_REVISION).value
)
assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER
assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == BRIDGE_SERIAL_NUMBER

View File

@@ -399,4 +399,4 @@ async def test_empty_name(hass, hk_driver):
assert acc.category == 10 # Sensor
assert acc.char_humidity.value == 20
assert acc.display_name is None
assert acc.display_name == "None"

View File

@@ -15,6 +15,8 @@ from homeassistant.components.climate.const import (
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_PRESET_MODE,
ATTR_PRESET_MODES,
ATTR_SWING_MODE,
ATTR_SWING_MODES,
ATTR_TARGET_TEMP_HIGH,
@@ -74,6 +76,7 @@ from homeassistant.components.homekit.type_thermostats import (
from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
CONF_TEMPERATURE_UNIT,
@@ -2349,3 +2352,127 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events):
assert len(call_set_fan_mode) == 2
assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_OFF
async def test_thermostat_with_fan_modes_set_to_none(hass, hk_driver, events):
"""Test a thermostate with fan modes set to None."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_FAN_MODE
| SUPPORT_SWING_MODE,
ATTR_FAN_MODES: None,
ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL],
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_SWING_MODE: SWING_BOTH,
ATTR_HVAC_MODES: [
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_COOL,
HVAC_MODE_OFF,
HVAC_MODE_AUTO,
],
},
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
assert acc.char_heating_thresh_temp.value == 19.0
assert acc.ordered_fan_speeds == []
assert CHAR_ROTATION_SPEED not in acc.fan_chars
assert CHAR_TARGET_FAN_STATE not in acc.fan_chars
assert CHAR_SWING_MODE in acc.fan_chars
assert CHAR_CURRENT_FAN_STATE in acc.fan_chars
async def test_thermostat_with_fan_modes_set_to_none_not_supported(
hass, hk_driver, events
):
"""Test a thermostate with fan modes set to None and supported feature missing."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_SWING_MODE,
ATTR_FAN_MODES: None,
ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL],
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_SWING_MODE: SWING_BOTH,
ATTR_HVAC_MODES: [
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_COOL,
HVAC_MODE_OFF,
HVAC_MODE_AUTO,
],
},
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
assert acc.char_cooling_thresh_temp.value == 23.0
assert acc.char_heating_thresh_temp.value == 19.0
assert acc.ordered_fan_speeds == []
assert CHAR_ROTATION_SPEED not in acc.fan_chars
assert CHAR_TARGET_FAN_STATE not in acc.fan_chars
assert CHAR_SWING_MODE in acc.fan_chars
assert CHAR_CURRENT_FAN_STATE in acc.fan_chars
async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set(
hass, hk_driver, events
):
"""Test a thermostate with fan mode and supported feature missing."""
entity_id = "climate.test"
hass.states.async_set(
entity_id,
HVAC_MODE_OFF,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE,
ATTR_MIN_TEMP: 44.6,
ATTR_MAX_TEMP: 95,
ATTR_PRESET_MODES: ["home", "away"],
ATTR_TEMPERATURE: 67,
ATTR_TARGET_TEMP_HIGH: None,
ATTR_TARGET_TEMP_LOW: None,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_FAN_MODES: None,
ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE,
ATTR_FAN_MODE: FAN_AUTO,
ATTR_PRESET_MODE: "home",
ATTR_FRIENDLY_NAME: "Rec Room",
ATTR_HVAC_MODES: [
HVAC_MODE_OFF,
HVAC_MODE_HEAT,
],
},
)
await hass.async_block_till_done()
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
hk_driver.add_accessory(acc)
await acc.run()
await hass.async_block_till_done()
assert acc.ordered_fan_speeds == []
assert not acc.fan_chars

View File

@@ -30,6 +30,7 @@ from homeassistant.components.homekit.util import (
async_port_is_available,
async_show_setup_message,
cleanup_name_for_homekit,
coerce_int,
convert_to_float,
density_to_air_quality,
format_version,
@@ -349,13 +350,23 @@ async def test_format_version():
assert format_version("undefined-undefined-1.6.8") == "1.6.8"
assert format_version("56.0-76060") == "56.0.76060"
assert format_version(3.6) == "3.6"
assert format_version("AK001-ZJ100") == "001.100"
assert format_version("AK001-ZJ100") == "1.100"
assert format_version("HF-LPB100-") == "100"
assert format_version("AK001-ZJ2149") == "001.2149"
assert format_version("AK001-ZJ2149") == "1.2149"
assert format_version("13216407885") == "4294967295" # max value
assert format_version("000132 16407885") == "132.16407885"
assert format_version("0.1") == "0.1"
assert format_version("0") is None
assert format_version("unknown") is None
async def test_coerce_int():
"""Test coerce_int method."""
assert coerce_int("1") == 1
assert coerce_int("") == 0
assert coerce_int(0) == 0
async def test_accessory_friendly_name():
"""Test we provide a helpful friendly name."""

View File

@@ -333,6 +333,43 @@ async def test_set_fan_mode(hass, mqtt_mock):
assert state.attributes.get("fan_mode") == "high"
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
@pytest.mark.parametrize(
"send_if_off,assert_async_publish",
[
({}, [call("fan-mode-topic", "low", 0, False)]),
({"send_if_off": True}, [call("fan-mode-topic", "low", 0, False)]),
({"send_if_off": False}, []),
],
)
async def test_set_fan_mode_send_if_off(
hass, mqtt_mock, send_if_off, assert_async_publish
):
"""Test setting of fan mode if the hvac is off."""
config = copy.deepcopy(DEFAULT_CONFIG)
config[CLIMATE_DOMAIN].update(send_if_off)
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_CLIMATE) is not None
# Turn on HVAC
await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE)
mqtt_mock.async_publish.reset_mock()
# Updates for fan_mode should be sent when the device is turned on
await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with("fan-mode-topic", "high", 0, False)
# Turn off HVAC
await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "off"
# Updates for fan_mode should be sent if SEND_IF_OFF is not set or is True
mqtt_mock.async_publish.reset_mock()
await common.async_set_fan_mode(hass, "low", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_has_calls(assert_async_publish)
async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog):
"""Test setting swing mode without required attribute."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG)
@@ -385,6 +422,43 @@ async def test_set_swing(hass, mqtt_mock):
assert state.attributes.get("swing_mode") == "on"
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
@pytest.mark.parametrize(
"send_if_off,assert_async_publish",
[
({}, [call("swing-mode-topic", "on", 0, False)]),
({"send_if_off": True}, [call("swing-mode-topic", "on", 0, False)]),
({"send_if_off": False}, []),
],
)
async def test_set_swing_mode_send_if_off(
hass, mqtt_mock, send_if_off, assert_async_publish
):
"""Test setting of swing mode if the hvac is off."""
config = copy.deepcopy(DEFAULT_CONFIG)
config[CLIMATE_DOMAIN].update(send_if_off)
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_CLIMATE) is not None
# Turn on HVAC
await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE)
mqtt_mock.async_publish.reset_mock()
# Updates for swing_mode should be sent when the device is turned on
await common.async_set_swing_mode(hass, "off", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with("swing-mode-topic", "off", 0, False)
# Turn off HVAC
await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "off"
# Updates for swing_mode should be sent if SEND_IF_OFF is not set or is True
mqtt_mock.async_publish.reset_mock()
await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_has_calls(assert_async_publish)
async def test_set_target_temperature(hass, mqtt_mock):
"""Test setting the target temperature."""
assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG)
@@ -421,6 +495,45 @@ async def test_set_target_temperature(hass, mqtt_mock):
mqtt_mock.async_publish.reset_mock()
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
@pytest.mark.parametrize(
"send_if_off,assert_async_publish",
[
({}, [call("temperature-topic", "21.0", 0, False)]),
({"send_if_off": True}, [call("temperature-topic", "21.0", 0, False)]),
({"send_if_off": False}, []),
],
)
async def test_set_target_temperature_send_if_off(
hass, mqtt_mock, send_if_off, assert_async_publish
):
"""Test setting of target temperature if the hvac is off."""
config = copy.deepcopy(DEFAULT_CONFIG)
config[CLIMATE_DOMAIN].update(send_if_off)
assert await async_setup_component(hass, CLIMATE_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_CLIMATE) is not None
# Turn on HVAC
await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE)
mqtt_mock.async_publish.reset_mock()
# Updates for target temperature should be sent when the device is turned on
await common.async_set_temperature(hass, 16.0, ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_called_once_with(
"temperature-topic", "16.0", 0, False
)
# Turn off HVAC
await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE)
state = hass.states.get(ENTITY_CLIMATE)
assert state.state == "off"
# Updates for target temperature sent should be if SEND_IF_OFF is not set or is True
mqtt_mock.async_publish.reset_mock()
await common.async_set_temperature(hass, 21.0, ENTITY_CLIMATE)
mqtt_mock.async_publish.assert_has_calls(assert_async_publish)
async def test_set_target_temperature_pessimistic(hass, mqtt_mock):
"""Test setting the target temperature."""
config = copy.deepcopy(DEFAULT_CONFIG)

View File

@@ -3,8 +3,9 @@ from unittest.mock import patch
import pytest
import voluptuous as vol
import yaml
from homeassistant import config_entries, data_entry_flow
from homeassistant import config as hass_config, config_entries, data_entry_flow
from homeassistant.components import mqtt
from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.core import HomeAssistant
@@ -151,7 +152,7 @@ async def test_manual_config_set(
"discovery": True,
}
# Check we tried the connection, with precedence for config entry settings
mock_try_connection.assert_called_once_with("127.0.0.1", 1883, None, None)
mock_try_connection.assert_called_once_with(hass, "127.0.0.1", 1883, None, None)
# Check config entry got setup
assert len(mock_finish_setup.mock_calls) == 1
@@ -642,3 +643,95 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection):
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
}
async def test_try_connection_with_advanced_parameters(
hass, mock_try_connection_success, tmp_path
):
"""Test config flow with advanced parameters from config."""
# Mock certificate files
certfile = tmp_path / "cert.pem"
certfile.write_text("## mock certificate file ##")
keyfile = tmp_path / "key.pem"
keyfile.write_text("## mock key file ##")
config = {
"certificate": "auto",
"tls_insecure": True,
"client_cert": certfile,
"client_key": keyfile,
}
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config = yaml.dump({mqtt.DOMAIN: config})
new_yaml_config_file.write_text(new_yaml_config)
assert new_yaml_config_file.read_text() == new_yaml_config
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
config_entry.add_to_hass(hass)
config_entry.data = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_BIRTH_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/online",
mqtt.ATTR_PAYLOAD: "online",
mqtt.ATTR_QOS: 1,
mqtt.ATTR_RETAIN: True,
},
mqtt.CONF_WILL_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/offline",
mqtt.ATTR_PAYLOAD: "offline",
mqtt.ATTR_QOS: 2,
mqtt.ATTR_RETAIN: False,
},
}
# Test default/suggested values from config
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "broker"
defaults = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
}
suggested = {
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
}
for k, v in defaults.items():
assert get_default(result["data_schema"].schema, k) == v
for k, v in suggested.items():
assert get_suggested(result["data_schema"].schema, k) == v
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345,
mqtt.CONF_USERNAME: "us3r",
mqtt.CONF_PASSWORD: "p4ss",
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
# check if the username and password was set from config flow and not from configuration.yaml
assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == (
"us3r",
"p4ss",
)
# check if tls_insecure_set is called
assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,)
# check if the certificate settings were set from configuration.yaml
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
"certfile"
] == str(certfile)
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
"keyfile"
] == str(keyfile)

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from unittest.mock import MagicMock, patch
import pytest
from rokuecp import RokuError
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
from homeassistant.components.media_player import MediaPlayerDeviceClass
from homeassistant.components.media_player.const import (
@@ -164,10 +164,15 @@ async def test_tv_setup(
assert device_entry.suggested_area == "Living room"
@pytest.mark.parametrize(
"error",
[RokuConnectionTimeoutError, RokuConnectionError, RokuError],
)
async def test_availability(
hass: HomeAssistant,
mock_roku: MagicMock,
mock_config_entry: MockConfigEntry,
error: RokuError,
) -> None:
"""Test entity availability."""
now = dt_util.utcnow()
@@ -179,7 +184,7 @@ async def test_availability(
await hass.async_block_till_done()
with patch("homeassistant.util.dt.utcnow", return_value=future):
mock_roku.update.side_effect = RokuError
mock_roku.update.side_effect = error
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE

View File

@@ -9,6 +9,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ENTITY_MATCH_ALL,
SERVICE_TURN_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import State
@@ -177,6 +178,34 @@ async def test_restore_state(hass, entities, enable_custom_integrations):
assert hass.states.get("scene.test").state == "2021-01-01T23:59:59+00:00"
async def test_restore_state_does_not_restore_unavailable(
hass, entities, enable_custom_integrations
):
"""Test we restore state integration but ignore unavailable."""
mock_restore_cache(hass, (State("scene.test", STATE_UNAVAILABLE),))
light_1, light_2 = await setup_lights(hass, entities)
assert await async_setup_component(
hass,
scene.DOMAIN,
{
"scene": [
{
"name": "test",
"entities": {
light_1.entity_id: "on",
light_2.entity_id: "on",
},
}
]
},
)
await hass.async_block_till_done()
assert hass.states.get("scene.test").state == STATE_UNKNOWN
async def activate(hass, entity_id=ENTITY_MATCH_ALL):
"""Activate a scene."""
data = {}

View File

@@ -115,7 +115,9 @@ async def test_query_limit(hass: HomeAssistant) -> None:
assert state.attributes["value"] == 5
async def test_query_no_value(hass: HomeAssistant) -> None:
async def test_query_no_value(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test the SQL sensor with a query that returns no value."""
config = {
"sensor": {
@@ -137,6 +139,9 @@ async def test_query_no_value(hass: HomeAssistant) -> None:
state = hass.states.get("sensor.count_tables")
assert state.state == STATE_UNKNOWN
text = "SELECT 5 as value where 1=2 returned no results"
assert text in caplog.text
async def test_invalid_query(hass: HomeAssistant) -> None:
"""Test the SQL sensor for invalid queries."""

View File

@@ -545,6 +545,22 @@ async def test_async_remove_runs_callbacks(hass):
assert len(result) == 1
async def test_async_remove_ignores_in_flight_polling(hass):
"""Test in flight polling is ignored after removing."""
result = []
ent = entity.Entity()
ent.hass = hass
ent.entity_id = "test.test"
ent.async_on_remove(lambda: result.append(1))
ent.async_write_ha_state()
assert hass.states.get("test.test").state == STATE_UNKNOWN
await ent.async_remove()
assert len(result) == 1
assert hass.states.get("test.test") is None
ent.async_write_ha_state()
async def test_set_context(hass):
"""Test setting context."""
context = Context()

View File

@@ -390,6 +390,30 @@ async def test_async_remove_with_platform(hass):
assert len(hass.states.async_entity_ids()) == 0
async def test_async_remove_with_platform_update_finishes(hass):
"""Remove an entity when an update finishes after its been removed."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
entity1 = MockEntity(name="test_1")
async def _delayed_update(*args, **kwargs):
await asyncio.sleep(0.01)
entity1.async_update = _delayed_update
# Add, remove, add, remove and make sure no updates
# cause the entity to reappear after removal
for i in range(2):
await component.async_add_entities([entity1])
assert len(hass.states.async_entity_ids()) == 1
entity1.async_write_ha_state()
assert hass.states.get(entity1.entity_id) is not None
task = asyncio.create_task(entity1.async_update_ha_state(True))
await entity1.async_remove()
assert len(hass.states.async_entity_ids()) == 0
await task
assert len(hass.states.async_entity_ids()) == 0
async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog):
"""Test for not adding duplicate entities."""
caplog.set_level(logging.ERROR)

View File

@@ -11,6 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries, setup
from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
from homeassistant.helpers.config_validation import (
PLATFORM_SCHEMA,
@@ -621,6 +622,22 @@ async def test_integration_disabled(hass, caplog):
assert disabled_reason in caplog.text
async def test_integration_logs_is_custom(hass, caplog):
"""Test we highlight it's a custom component when errors happen."""
mock_integration(
hass,
MockModule("test_component1"),
built_in=False,
)
with patch(
"homeassistant.setup.async_process_deps_reqs",
side_effect=HomeAssistantError("Boom"),
):
result = await setup.async_setup_component(hass, "test_component1", {})
assert not result
assert "Setup failed for custom integration test_component1: Boom" in caplog.text
async def test_async_get_loaded_integrations(hass):
"""Test we can enumerate loaded integations."""
hass.config.components.add("notbase")