This commit is contained in:
Paulus Schoutsen 2023-06-15 00:19:30 -04:00 committed by GitHub
commit e5c5790768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 976 additions and 257 deletions

View File

@ -1,11 +1,11 @@
image: homeassistant/{arch}-homeassistant
shadow_repository: ghcr.io/home-assistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.05.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.05.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.05.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.05.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.05.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@ -430,7 +430,9 @@ INFERRED_UNITS = {
" Percent": PERCENTAGE,
" Volts": UnitOfElectricPotential.VOLT,
" Ampere": UnitOfElectricCurrent.AMPERE,
" Amps": UnitOfElectricCurrent.AMPERE,
" Volt-Ampere": UnitOfApparentPower.VOLT_AMPERE,
" VA": UnitOfApparentPower.VOLT_AMPERE,
" Watts": UnitOfPower.WATT,
" Hz": UnitOfFrequency.HERTZ,
" C": UnitOfTemperature.CELSIUS,

View File

@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==1.5.1", "yalexs-ble==2.1.17"]
"requirements": ["yalexs==1.5.1", "yalexs-ble==2.1.18"]
}

View File

@ -46,12 +46,15 @@ from homeassistant.const import (
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
SERVICE_RELOAD,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
@ -163,14 +166,39 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Command Line from yaml config."""
command_line_config: list[dict[str, dict[str, Any]]] = config.get(DOMAIN, [])
async def _reload_config(call: Event | ServiceCall) -> None:
"""Reload Command Line."""
reload_config = await async_integration_yaml_config(hass, "command_line")
reset_platforms = async_get_platforms(hass, "command_line")
for reset_platform in reset_platforms:
_LOGGER.debug("Reload resetting platform: %s", reset_platform.domain)
await reset_platform.async_reset()
if not reload_config:
return
await async_load_platforms(hass, reload_config.get(DOMAIN, []), reload_config)
async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config)
await async_load_platforms(hass, config.get(DOMAIN, []), config)
return True
async def async_load_platforms(
hass: HomeAssistant,
command_line_config: list[dict[str, dict[str, Any]]],
config: ConfigType,
) -> None:
"""Load platforms from yaml."""
if not command_line_config:
return True
return
_LOGGER.debug("Full config loaded: %s", command_line_config)
load_coroutines: list[Coroutine[Any, Any, None]] = []
platforms: list[Platform] = []
reload_configs: list[tuple] = []
for platform_config in command_line_config:
for platform, _config in platform_config.items():
if (mapped_platform := PLATFORM_MAPPING[platform]) not in platforms:
@ -180,6 +208,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
platform_config,
PLATFORM_MAPPING[platform],
)
reload_configs.append((PLATFORM_MAPPING[platform], _config))
load_coroutines.append(
discovery.async_load_platform(
hass,
@ -190,10 +219,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
)
await async_setup_reload_service(hass, DOMAIN, platforms)
if load_coroutines:
_LOGGER.debug("Loading platforms: %s", platforms)
await asyncio.gather(*load_coroutines)
return True

View File

@ -30,6 +30,7 @@ from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .sensor import CommandSensorData
@ -183,3 +184,10 @@ class CommandBinarySensor(BinarySensorEntity):
self._attr_is_on = False
self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())

View File

@ -31,7 +31,7 @@ from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util, slugify
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
@ -220,6 +220,13 @@ class CommandCover(CoverEntity):
self._state = int(payload)
await self.async_update_ha_state(True)
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self.hass.async_add_executor_job(self._move_cover, self._command_open)

View File

@ -33,6 +33,7 @@ from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import check_output_or_log
@ -200,6 +201,13 @@ class CommandSensor(SensorEntity):
self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())
class CommandSensorData:
"""The class for handling the data retrieval."""

View File

@ -34,7 +34,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
from homeassistant.helpers.template import Template
from homeassistant.helpers.template_entity import ManualTriggerEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util, slugify
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
@ -240,6 +240,13 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
self._process_manual_data(payload)
await self.async_update_ha_state(True)
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if await self._switch(self._command_on) and not self._command_state:

View File

@ -7,6 +7,6 @@
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"quality_scale": "platinum",
"requirements": ["pydaikin==2.9.1"],
"requirements": ["pydaikin==2.9.0"],
"zeroconf": ["_dkapi._tcp.local."]
}

View File

@ -42,7 +42,7 @@ async def async_setup_entry(
[
DaikinZoneSwitch(daikin_api, zone_id)
for zone_id, zone in enumerate(zones)
if zone[0] != ("-", "0")
if zone != ("-", "0")
]
)
if daikin_api.device.support_advanced_modes:

View File

@ -250,7 +250,7 @@ class ElectraClimateEntity(ClimateEntity):
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
raise ValueError("No target temperature provided")
self._electra_ac_device.set_temperature(temperature)
self._electra_ac_device.set_temperature(int(temperature))
await self._async_operate_electra_ac()
def _update_device_attrs(self) -> None:

View File

@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/elkm1",
"iot_class": "local_push",
"loggers": ["elkm1_lib"],
"requirements": ["elkm1-lib==2.2.2"]
"requirements": ["elkm1-lib==2.2.5"]
}

View File

@ -96,19 +96,29 @@ class MultiprotocolAddonManager(AddonManager):
) -> None:
"""Register a multipan platform."""
self._platforms[integration_domain] = platform
if self._channel is not None or not await platform.async_using_multipan(hass):
channel = await platform.async_get_channel(hass)
using_multipan = await platform.async_using_multipan(hass)
_LOGGER.info(
"Registering new multipan platform '%s', using multipan: %s, channel: %s",
integration_domain,
using_multipan,
channel,
)
if self._channel is not None or not using_multipan:
return
new_channel = await platform.async_get_channel(hass)
if new_channel is None:
if channel is None:
return
_LOGGER.info(
"Setting multipan channel to %s (source: '%s')",
new_channel,
channel,
integration_domain,
)
self.async_set_channel(new_channel)
self.async_set_channel(channel)
async def async_change_channel(
self, channel: int, delay: float

View File

@ -17,7 +17,7 @@
"iot_class": "local_push",
"loggers": ["pyinsteon", "pypubsub"],
"requirements": [
"pyinsteon==1.4.2",
"pyinsteon==1.4.3",
"insteon-frontend-home-assistant==0.3.5"
],
"usb": [

View File

@ -19,21 +19,18 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up IPP from a config entry."""
hass.data.setdefault(DOMAIN, {})
if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)):
# Create IPP instance for this entry
coordinator = IPPDataUpdateCoordinator(
hass,
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
base_path=entry.data[CONF_BASE_PATH],
tls=entry.data[CONF_SSL],
verify_ssl=entry.data[CONF_VERIFY_SSL],
)
hass.data[DOMAIN][entry.entry_id] = coordinator
coordinator = IPPDataUpdateCoordinator(
hass,
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
base_path=entry.data[CONF_BASE_PATH],
tls=entry.data[CONF_SSL],
verify_ssl=entry.data[CONF_VERIFY_SSL],
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@ -41,7 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -1,11 +1,14 @@
"""Receive signals from a keyboard and use it as a remote control."""
# pylint: disable=import-error
from __future__ import annotations
import asyncio
from contextlib import suppress
import logging
import os
from typing import Any
import aionotify
from asyncinotify import Inotify, Mask
from evdev import InputDevice, categorize, ecodes, list_devices
import voluptuous as vol
@ -64,9 +67,9 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the keyboard_remote."""
config = config[DOMAIN]
domain_config: list[dict[str, Any]] = config[DOMAIN]
remote = KeyboardRemote(hass, config)
remote = KeyboardRemote(hass, domain_config)
remote.setup()
return True
@ -75,12 +78,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class KeyboardRemote:
"""Manage device connection/disconnection using inotify to asynchronously monitor."""
def __init__(self, hass, config):
def __init__(self, hass: HomeAssistant, config: list[dict[str, Any]]) -> None:
"""Create handlers and setup dictionaries to keep track of them."""
self.hass = hass
self.handlers_by_name = {}
self.handlers_by_descriptor = {}
self.active_handlers_by_descriptor = {}
self.active_handlers_by_descriptor: dict[str, asyncio.Future] = {}
self.inotify = None
self.watcher = None
self.monitor_task = None
@ -110,16 +114,12 @@ class KeyboardRemote:
connected, and start monitoring for device connection/disconnection.
"""
# start watching
self.watcher = aionotify.Watcher()
self.watcher.watch(
alias="devinput",
path=DEVINPUT,
flags=aionotify.Flags.CREATE
| aionotify.Flags.ATTRIB
| aionotify.Flags.DELETE,
_LOGGER.debug("Start monitoring")
self.inotify = Inotify()
self.watcher = self.inotify.add_watch(
DEVINPUT, Mask.CREATE | Mask.ATTRIB | Mask.DELETE
)
await self.watcher.setup(self.hass.loop)
# add initial devices (do this AFTER starting watcher in order to
# avoid race conditions leading to missing device connections)
@ -134,7 +134,9 @@ class KeyboardRemote:
continue
self.active_handlers_by_descriptor[descriptor] = handler
initial_start_monitoring.add(handler.async_start_monitoring(dev))
initial_start_monitoring.add(
asyncio.create_task(handler.async_device_start_monitoring(dev))
)
if initial_start_monitoring:
await asyncio.wait(initial_start_monitoring)
@ -146,6 +148,10 @@ class KeyboardRemote:
_LOGGER.debug("Cleanup on shutdown")
if self.inotify and self.watcher:
self.inotify.rm_watch(self.watcher)
self.watcher = None
if self.monitor_task is not None:
if not self.monitor_task.done():
self.monitor_task.cancel()
@ -153,11 +159,16 @@ class KeyboardRemote:
handler_stop_monitoring = set()
for handler in self.active_handlers_by_descriptor.values():
handler_stop_monitoring.add(handler.async_stop_monitoring())
handler_stop_monitoring.add(
asyncio.create_task(handler.async_device_stop_monitoring())
)
if handler_stop_monitoring:
await asyncio.wait(handler_stop_monitoring)
if self.inotify:
self.inotify.close()
self.inotify = None
def get_device_handler(self, descriptor):
"""Find the correct device handler given a descriptor (path)."""
@ -187,20 +198,21 @@ class KeyboardRemote:
async def async_monitor_devices(self):
"""Monitor asynchronously for device connection/disconnection or permissions changes."""
_LOGGER.debug("Start monitoring loop")
try:
while True:
event = await self.watcher.get_event()
async for event in self.inotify:
descriptor = f"{DEVINPUT}/{event.name}"
_LOGGER.debug("got events for %s: %s", descriptor, event.mask)
descriptor_active = descriptor in self.active_handlers_by_descriptor
if (event.flags & aionotify.Flags.DELETE) and descriptor_active:
if (event.mask & Mask.DELETE) and descriptor_active:
handler = self.active_handlers_by_descriptor[descriptor]
del self.active_handlers_by_descriptor[descriptor]
await handler.async_stop_monitoring()
await handler.async_device_stop_monitoring()
elif (
(event.flags & aionotify.Flags.CREATE)
or (event.flags & aionotify.Flags.ATTRIB)
(event.mask & Mask.CREATE) or (event.mask & Mask.ATTRIB)
) and not descriptor_active:
dev, handler = await self.hass.async_add_executor_job(
self.get_device_handler, descriptor
@ -208,31 +220,32 @@ class KeyboardRemote:
if handler is None:
continue
self.active_handlers_by_descriptor[descriptor] = handler
await handler.async_start_monitoring(dev)
await handler.async_device_start_monitoring(dev)
except asyncio.CancelledError:
_LOGGER.debug("Monitoring canceled")
return
class DeviceHandler:
"""Manage input events using evdev with asyncio."""
def __init__(self, hass, dev_block):
def __init__(self, hass: HomeAssistant, dev_block: dict[str, Any]) -> None:
"""Fill configuration data."""
self.hass = hass
key_types = dev_block.get(TYPE)
key_types = dev_block[TYPE]
self.key_values = set()
for key_type in key_types:
self.key_values.add(KEY_VALUE[key_type])
self.emulate_key_hold = dev_block.get(EMULATE_KEY_HOLD)
self.emulate_key_hold_delay = dev_block.get(EMULATE_KEY_HOLD_DELAY)
self.emulate_key_hold_repeat = dev_block.get(EMULATE_KEY_HOLD_REPEAT)
self.emulate_key_hold = dev_block[EMULATE_KEY_HOLD]
self.emulate_key_hold_delay = dev_block[EMULATE_KEY_HOLD_DELAY]
self.emulate_key_hold_repeat = dev_block[EMULATE_KEY_HOLD_REPEAT]
self.monitor_task = None
self.dev = None
async def async_keyrepeat(self, path, name, code, delay, repeat):
async def async_device_keyrepeat(self, path, name, code, delay, repeat):
"""Emulate keyboard delay/repeat behaviour by sending key events on a timer."""
await asyncio.sleep(delay)
@ -248,8 +261,9 @@ class KeyboardRemote:
)
await asyncio.sleep(repeat)
async def async_start_monitoring(self, dev):
async def async_device_start_monitoring(self, dev):
"""Start event monitoring task and issue event."""
_LOGGER.debug("Keyboard async_device_start_monitoring, %s", dev.name)
if self.monitor_task is None:
self.dev = dev
self.monitor_task = self.hass.async_create_task(
@ -261,7 +275,7 @@ class KeyboardRemote:
)
_LOGGER.debug("Keyboard (re-)connected, %s", dev.name)
async def async_stop_monitoring(self):
async def async_device_stop_monitoring(self):
"""Stop event monitoring task and issue event."""
if self.monitor_task is not None:
with suppress(OSError):
@ -295,6 +309,7 @@ class KeyboardRemote:
_LOGGER.debug("Start device monitoring")
await self.hass.async_add_executor_job(dev.grab)
async for event in dev.async_read_loop():
# pylint: disable=no-member
if event.type is ecodes.EV_KEY:
if event.value in self.key_values:
_LOGGER.debug(categorize(event))
@ -313,7 +328,7 @@ class KeyboardRemote:
and self.emulate_key_hold
):
repeat_tasks[event.code] = self.hass.async_create_task(
self.async_keyrepeat(
self.async_device_keyrepeat(
dev.path,
dev.name,
event.code,

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/keyboard_remote",
"iot_class": "local_push",
"loggers": ["aionotify", "evdev"],
"requirements": ["evdev==1.4.0", "aionotify==0.2.0"]
"requirements": ["evdev==1.6.1", "asyncinotify==4.0.2"]
}

View File

@ -12,7 +12,7 @@
"quality_scale": "platinum",
"requirements": [
"xknx==2.10.0",
"xknxproject==3.1.0",
"knx_frontend==2023.5.31.141540"
"xknxproject==3.1.1",
"knx-frontend==2023.6.9.195839"
]
}

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Final
from knx_frontend import get_build_id, locate_dir
from knx_frontend import entrypoint_js, is_dev_build, locate_dir
import voluptuous as vol
from xknx.telegram import TelegramDirection
from xknxproject.exceptions import XknxProjectException
@ -31,9 +31,10 @@ async def register_panel(hass: HomeAssistant) -> None:
if DOMAIN not in hass.data.get("frontend_panels", {}):
path = locate_dir()
build_id = get_build_id()
hass.http.register_static_path(
URL_BASE, path, cache_headers=(build_id != "dev")
URL_BASE,
path,
cache_headers=not is_dev_build,
)
await panel_custom.async_register_panel(
hass=hass,
@ -41,12 +42,13 @@ async def register_panel(hass: HomeAssistant) -> None:
webcomponent_name="knx-frontend",
sidebar_title=DOMAIN.upper(),
sidebar_icon="mdi:bus-electric",
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
module_url=f"{URL_BASE}/{entrypoint_js()}",
embed_iframe=True,
require_admin=True,
)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "knx/info",
@ -129,6 +131,7 @@ async def ws_project_file_remove(
connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "knx/group_monitor_info",
@ -155,6 +158,7 @@ def ws_group_monitor_info(
)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "knx/subscribe_telegrams",

View File

@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"requirements": ["pylitterbot==2023.4.0"]
"requirements": ["pylitterbot==2023.4.2"]
}

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/noaa_tides",
"iot_class": "cloud_polling",
"loggers": ["noaa_coops"],
"requirements": ["noaa-coops==0.1.8"]
"requirements": ["noaa-coops==0.1.9"]
}

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/opple",
"iot_class": "local_polling",
"loggers": ["pyoppleio"],
"requirements": ["pyoppleio==1.0.5"]
"requirements": ["pyoppleio-legacy==1.0.8"]
}

View File

@ -10,7 +10,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["regenmaschine"],
"requirements": ["regenmaschine==2023.05.1"],
"requirements": ["regenmaschine==2023.06.0"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@ -22,13 +22,11 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Roku from a config entry."""
hass.data.setdefault(DOMAIN, {})
if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)):
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
hass.data[DOMAIN][entry.entry_id] = coordinator
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@ -36,7 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/russound_rio",
"iot_class": "local_push",
"loggers": ["russound_rio"],
"requirements": ["russound_rio==0.1.8"]
"requirements": ["russound-rio==1.0.0"]
}

View File

@ -535,7 +535,10 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
async def shutdown(self) -> None:
"""Shutdown the coordinator."""
if self.device.connected:
await async_stop_scanner(self.device)
try:
await async_stop_scanner(self.device)
except InvalidAuthError:
self.entry.async_start_reauth(self.hass)
await self.device.shutdown()
await self._async_disconnected()

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/sisyphus",
"iot_class": "local_push",
"loggers": ["sisyphus_control"],
"requirements": ["sisyphus-control==3.1.2"]
"requirements": ["sisyphus-control==3.1.3"]
}

View File

@ -41,7 +41,7 @@
"iot_class": "local_push",
"loggers": ["pyunifiprotect", "unifi_discovery"],
"quality_scale": "platinum",
"requirements": ["pyunifiprotect==4.10.2", "unifi-discovery==1.1.7"],
"requirements": ["pyunifiprotect==4.10.3", "unifi-discovery==1.1.7"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@ -275,9 +275,7 @@ async def async_setup_entry(
if not entity_method:
continue
await entity_method(**params)
update_tasks.append(
hass.async_create_task(entity.async_update_ha_state(True))
)
update_tasks.append(asyncio.create_task(entity.async_update_ha_state(True)))
if update_tasks:
await asyncio.wait(update_tasks)

View File

@ -229,7 +229,9 @@ async def async_setup_entry(
if not hasattr(target_device, method["method"]):
continue
await getattr(target_device, method["method"])(**params)
update_tasks.append(target_device.async_update_ha_state(True))
update_tasks.append(
asyncio.create_task(target_device.async_update_ha_state(True))
)
if update_tasks:
await asyncio.wait(update_tasks)

View File

@ -441,7 +441,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity):
return await self._try_command(
"Setting delay off miio device failed.",
self._device.delay_off,
delay_off_countdown * 60,
delay_off_countdown,
)
async def async_set_led_brightness_level(self, level: int) -> bool:

View File

@ -500,7 +500,9 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities):
if not hasattr(device, method["method"]):
continue
await getattr(device, method["method"])(**params)
update_tasks.append(device.async_update_ha_state(True))
update_tasks.append(
asyncio.create_task(device.async_update_ha_state(True))
)
if update_tasks:
await asyncio.wait(update_tasks)

View File

@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"iot_class": "local_push",
"requirements": ["yalexs-ble==2.1.17"]
"requirements": ["yalexs-ble==2.1.18"]
}

View File

@ -22,7 +22,13 @@ from homeassistant.helpers.selector import (
SelectSelectorConfig,
)
from .const import CONF_CHANNELS, DEFAULT_ACCESS, DOMAIN, LOGGER
from .const import (
CHANNEL_CREATION_HELP_URL,
CONF_CHANNELS,
DEFAULT_ACCESS,
DOMAIN,
LOGGER,
)
async def get_resource(hass: HomeAssistant, token: str) -> Resource:
@ -99,6 +105,11 @@ class OAuth2FlowHandler(
response = await self.hass.async_add_executor_job(
own_channel_request.execute
)
if not response["items"]:
return self.async_abort(
reason="no_channel",
description_placeholders={"support_url": CHANNEL_CREATION_HELP_URL},
)
own_channel = response["items"][0]
except HttpError as ex:
error = ex.reason

View File

@ -4,6 +4,7 @@ import logging
DEFAULT_ACCESS = ["https://www.googleapis.com/auth/youtube.readonly"]
DOMAIN = "youtube"
MANUFACTURER = "Google, Inc."
CHANNEL_CREATION_HELP_URL = "https://support.google.com/youtube/answer/1646861"
CONF_CHANNELS = "channels"
CONF_ID = "id"

View File

@ -2,6 +2,7 @@
"config": {
"abort": {
"access_not_configured": "Please read the below message we got from Google:\n\n{message}",
"no_channel": "Please create a YouTube channel to be able to use the integration. Instructions can be found at {support_url}.",
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"

View File

@ -907,6 +907,7 @@ async def websocket_bind_devices(
ATTR_TARGET_IEEE,
target_ieee,
)
connection.send_result(msg[ID])
@websocket_api.require_admin
@ -935,6 +936,7 @@ async def websocket_unbind_devices(
ATTR_TARGET_IEEE,
target_ieee,
)
connection.send_result(msg[ID])
@websocket_api.require_admin
@ -951,13 +953,14 @@ async def websocket_bind_group(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Directly bind a device to a group."""
zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
zha_gateway: ZHAGateway = get_gateway(hass)
source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE]
group_id: int = msg[GROUP_ID]
bindings: list[ClusterBinding] = msg[BINDINGS]
source_device = zha_gateway.get_device(source_ieee)
assert source_device
await source_device.async_bind_to_group(group_id, bindings)
connection.send_result(msg[ID])
@websocket_api.require_admin
@ -974,13 +977,19 @@ async def websocket_unbind_group(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Unbind a device from a group."""
zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
zha_gateway: ZHAGateway = get_gateway(hass)
source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE]
group_id: int = msg[GROUP_ID]
bindings: list[ClusterBinding] = msg[BINDINGS]
source_device = zha_gateway.get_device(source_ieee)
assert source_device
await source_device.async_unbind_from_group(group_id, bindings)
connection.send_result(msg[ID])
def get_gateway(hass: HomeAssistant) -> ZHAGateway:
"""Return Gateway, mainly as fixture for mocking during testing."""
return hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
async def async_binding_operation(

View File

@ -209,6 +209,9 @@ async def start_client(
LOGGER.info("Connection to Zwave JS Server initialized")
assert client.driver
async_dispatcher_send(
hass, f"{DOMAIN}_{client.driver.controller.home_id}_connected_to_server"
)
await driver_events.setup(client.driver)

View File

@ -101,7 +101,7 @@ RESET_METER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
SET_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): SERVICE_SET_CONFIG_PARAMETER,
vol.Required(ATTR_ENDPOINT): vol.Coerce(int),
vol.Required(ATTR_ENDPOINT, default=0): vol.Coerce(int),
vol.Required(ATTR_CONFIG_PARAMETER): vol.Any(int, str),
vol.Required(ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(None, int, str),
vol.Required(ATTR_VALUE): vol.Coerce(int),

View File

@ -161,7 +161,7 @@ BASE_VALUE_UPDATED_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]),
vol.Required(ATTR_PROPERTY): vol.Any(int, str),
vol.Optional(ATTR_PROPERTY_KEY): vol.Any(None, vol.Coerce(int), str),
vol.Optional(ATTR_ENDPOINT): vol.Any(None, vol.Coerce(int)),
vol.Optional(ATTR_ENDPOINT, default=0): vol.Any(None, vol.Coerce(int)),
vol.Optional(ATTR_FROM): VALUE_SCHEMA,
vol.Optional(ATTR_TO): VALUE_SCHEMA,
}

View File

@ -1,18 +1,20 @@
"""Offer Z-Wave JS event listening automation trigger."""
from __future__ import annotations
from collections.abc import Callable
import functools
from pydantic import ValidationError
import voluptuous as vol
from zwave_js_server.client import Client
from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP
from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP
from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP, Driver
from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
@ -150,7 +152,7 @@ async def async_attach_trigger(
event_name = config[ATTR_EVENT]
event_data_filter = config.get(ATTR_EVENT_DATA, {})
unsubs = []
unsubs: list[Callable] = []
job = HassJob(action)
trigger_data = trigger_info["trigger_data"]
@ -199,26 +201,6 @@ async def async_attach_trigger(
hass.async_run_hass_job(job, {"trigger": payload})
if not nodes:
entry_id = config[ATTR_CONFIG_ENTRY_ID]
client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
assert client.driver
if event_source == "controller":
unsubs.append(client.driver.controller.on(event_name, async_on_event))
else:
unsubs.append(client.driver.on(event_name, async_on_event))
for node in nodes:
driver = node.client.driver
assert driver is not None # The node comes from the driver.
device_identifier = get_device_id(driver, node)
device = dev_reg.async_get_device({device_identifier})
assert device
# We need to store the device for the callback
unsubs.append(
node.on(event_name, functools.partial(async_on_event, device=device))
)
@callback
def async_remove() -> None:
"""Remove state listeners async."""
@ -226,4 +208,45 @@ async def async_attach_trigger(
unsub()
unsubs.clear()
@callback
def _create_zwave_listeners() -> None:
"""Create Z-Wave JS listeners."""
async_remove()
# Nodes list can come from different drivers and we will need to listen to
# server connections for all of them.
drivers: set[Driver] = set()
if not nodes:
entry_id = config[ATTR_CONFIG_ENTRY_ID]
client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT]
driver = client.driver
assert driver
drivers.add(driver)
if event_source == "controller":
unsubs.append(driver.controller.on(event_name, async_on_event))
else:
unsubs.append(driver.on(event_name, async_on_event))
for node in nodes:
driver = node.client.driver
assert driver is not None # The node comes from the driver.
drivers.add(driver)
device_identifier = get_device_id(driver, node)
device = dev_reg.async_get_device({device_identifier})
assert device
# We need to store the device for the callback
unsubs.append(
node.on(event_name, functools.partial(async_on_event, device=device))
)
for driver in drivers:
unsubs.append(
async_dispatcher_connect(
hass,
f"{DOMAIN}_{driver.controller.home_id}_connected_to_server",
_create_zwave_listeners,
)
)
_create_zwave_listeners()
return async_remove

View File

@ -1,15 +1,18 @@
"""Offer Z-Wave JS value updated listening automation trigger."""
from __future__ import annotations
from collections.abc import Callable
import functools
import voluptuous as vol
from zwave_js_server.const import CommandClass
from zwave_js_server.model.driver import Driver
from zwave_js_server.model.value import Value, get_value_id_str
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
@ -99,7 +102,7 @@ async def async_attach_trigger(
property_ = config[ATTR_PROPERTY]
endpoint = config.get(ATTR_ENDPOINT)
property_key = config.get(ATTR_PROPERTY_KEY)
unsubs = []
unsubs: list[Callable] = []
job = HassJob(action)
trigger_data = trigger_info["trigger_data"]
@ -153,29 +156,11 @@ async def async_attach_trigger(
ATTR_PREVIOUS_VALUE_RAW: prev_value_raw,
ATTR_CURRENT_VALUE: curr_value,
ATTR_CURRENT_VALUE_RAW: curr_value_raw,
"description": f"Z-Wave value {value_id} updated on {device_name}",
"description": f"Z-Wave value {value.value_id} updated on {device_name}",
}
hass.async_run_hass_job(job, {"trigger": payload})
for node in nodes:
driver = node.client.driver
assert driver is not None # The node comes from the driver.
device_identifier = get_device_id(driver, node)
device = dev_reg.async_get_device({device_identifier})
assert device
value_id = get_value_id_str(
node, command_class, property_, endpoint, property_key
)
value = node.values[value_id]
# We need to store the current value and device for the callback
unsubs.append(
node.on(
"value updated",
functools.partial(async_on_value_updated, value, device),
)
)
@callback
def async_remove() -> None:
"""Remove state listeners async."""
@ -183,4 +168,40 @@ async def async_attach_trigger(
unsub()
unsubs.clear()
def _create_zwave_listeners() -> None:
"""Create Z-Wave JS listeners."""
async_remove()
# Nodes list can come from different drivers and we will need to listen to
# server connections for all of them.
drivers: set[Driver] = set()
for node in nodes:
driver = node.client.driver
assert driver is not None # The node comes from the driver.
drivers.add(driver)
device_identifier = get_device_id(driver, node)
device = dev_reg.async_get_device({device_identifier})
assert device
value_id = get_value_id_str(
node, command_class, property_, endpoint, property_key
)
value = node.values[value_id]
# We need to store the current value and device for the callback
unsubs.append(
node.on(
"value updated",
functools.partial(async_on_value_updated, value, device),
)
)
for driver in drivers:
unsubs.append(
async_dispatcher_connect(
hass,
f"{DOMAIN}_{driver.controller.home_id}_connected_to_server",
_create_zwave_listeners,
)
)
_create_zwave_listeners()
return async_remove

View File

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)

View File

@ -189,8 +189,6 @@ class DeviceFilterSelectorConfig(TypedDict, total=False):
integration: str
manufacturer: str
model: str
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
filter: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
class ActionSelectorConfig(TypedDict):
@ -546,14 +544,12 @@ class DateTimeSelector(Selector[DateTimeSelectorConfig]):
return data
class DeviceSelectorConfig(TypedDict, total=False):
class DeviceSelectorConfig(DeviceFilterSelectorConfig, total=False):
"""Class to represent a device selector config."""
integration: str
manufacturer: str
model: str
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
multiple: bool
filter: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
@SELECTORS.register("device")
@ -622,6 +618,7 @@ class EntitySelectorConfig(EntityFilterSelectorConfig, total=False):
exclude_entities: list[str]
include_entities: list[str]
multiple: bool
filter: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
@SELECTORS.register("entity")

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.6.1"
version = "2023.6.2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@ -222,9 +222,6 @@ aiomusiccast==0.14.8
# homeassistant.components.nanoleaf
aionanoleaf==0.2.1
# homeassistant.components.keyboard_remote
aionotify==0.2.0
# homeassistant.components.notion
aionotion==2023.05.5
@ -382,6 +379,9 @@ asterisk_mbox==0.5.0
# homeassistant.components.yeelight
async-upnp-client==0.33.2
# homeassistant.components.keyboard_remote
asyncinotify==4.0.2
# homeassistant.components.supla
asyncpysupla==0.0.5
@ -650,7 +650,7 @@ elgato==4.0.1
eliqonline==1.2.2
# homeassistant.components.elkm1
elkm1-lib==2.2.2
elkm1-lib==2.2.5
# homeassistant.components.elmax
elmax_api==0.0.4
@ -695,7 +695,7 @@ eternalegypt==0.0.16
eufylife_ble_client==0.1.7
# homeassistant.components.keyboard_remote
# evdev==1.4.0
# evdev==1.6.1
# homeassistant.components.evohome
evohome-async==0.3.15
@ -1032,7 +1032,7 @@ kegtron-ble==0.4.0
kiwiki-client==0.1.1
# homeassistant.components.knx
knx_frontend==2023.5.31.141540
knx-frontend==2023.6.9.195839
# homeassistant.components.konnected
konnected==1.2.0
@ -1218,7 +1218,7 @@ niko-home-control==0.2.1
niluclient==0.1.2
# homeassistant.components.noaa_tides
noaa-coops==0.1.8
noaa-coops==0.1.9
# homeassistant.components.nfandroidtv
notifications-android-tv==0.1.5
@ -1576,7 +1576,7 @@ pycsspeechtts==1.0.8
# pycups==1.9.73
# homeassistant.components.daikin
pydaikin==2.9.1
pydaikin==2.9.0
# homeassistant.components.danfoss_air
pydanfossair==0.1.0
@ -1699,7 +1699,7 @@ pyialarm==2.2.0
pyicloud==1.0.0
# homeassistant.components.insteon
pyinsteon==1.4.2
pyinsteon==1.4.3
# homeassistant.components.intesishome
pyintesishome==1.8.0
@ -1771,7 +1771,7 @@ pylibrespot-java==0.1.1
pylitejet==0.5.0
# homeassistant.components.litterrobot
pylitterbot==2023.4.0
pylitterbot==2023.4.2
# homeassistant.components.lutron_caseta
pylutron-caseta==0.18.1
@ -1861,7 +1861,7 @@ pyopenuv==2023.02.0
pyopnsense==0.2.0
# homeassistant.components.opple
pyoppleio==1.0.5
pyoppleio-legacy==1.0.8
# homeassistant.components.opentherm_gw
pyotgw==2.1.3
@ -2168,7 +2168,7 @@ pytrafikverket==0.3.3
pyudev==0.23.2
# homeassistant.components.unifiprotect
pyunifiprotect==4.10.2
pyunifiprotect==4.10.3
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@ -2246,7 +2246,7 @@ rapt-ble==0.1.1
raspyrfm-client==1.2.8
# homeassistant.components.rainmachine
regenmaschine==2023.05.1
regenmaschine==2023.06.0
# homeassistant.components.renault
renault-api==0.1.13
@ -2293,12 +2293,12 @@ rpi-bad-power==0.1.0
# homeassistant.components.rtsp_to_webrtc
rtsp-to-webrtc==0.5.1
# homeassistant.components.russound_rio
russound-rio==1.0.0
# homeassistant.components.russound_rnet
russound==0.1.9
# homeassistant.components.russound_rio
russound_rio==0.1.8
# homeassistant.components.ruuvitag_ble
ruuvitag-ble==0.1.1
@ -2367,7 +2367,7 @@ simplepush==2.1.1
simplisafe-python==2023.05.0
# homeassistant.components.sisyphus
sisyphus-control==3.1.2
sisyphus-control==3.1.3
# homeassistant.components.slack
slackclient==2.5.0
@ -2680,7 +2680,7 @@ xiaomi-ble==0.17.2
xknx==2.10.0
# homeassistant.components.knx
xknxproject==3.1.0
xknxproject==3.1.1
# homeassistant.components.bluesound
# homeassistant.components.fritz
@ -2698,7 +2698,7 @@ yalesmartalarmclient==0.3.9
# homeassistant.components.august
# homeassistant.components.yalexs_ble
yalexs-ble==2.1.17
yalexs-ble==2.1.18
# homeassistant.components.august
yalexs==1.5.1

View File

@ -515,7 +515,7 @@ easyenergy==0.3.0
elgato==4.0.1
# homeassistant.components.elkm1
elkm1-lib==2.2.2
elkm1-lib==2.2.5
# homeassistant.components.elmax
elmax_api==0.0.4
@ -791,7 +791,7 @@ justnimbus==0.6.0
kegtron-ble==0.4.0
# homeassistant.components.knx
knx_frontend==2023.5.31.141540
knx-frontend==2023.6.9.195839
# homeassistant.components.konnected
konnected==1.2.0
@ -1164,7 +1164,7 @@ pycoolmasternet-async==0.1.5
pycsspeechtts==1.0.8
# homeassistant.components.daikin
pydaikin==2.9.1
pydaikin==2.9.0
# homeassistant.components.deconz
pydeconz==112
@ -1248,7 +1248,7 @@ pyialarm==2.2.0
pyicloud==1.0.0
# homeassistant.components.insteon
pyinsteon==1.4.2
pyinsteon==1.4.3
# homeassistant.components.ipma
pyipma==3.0.6
@ -1302,7 +1302,7 @@ pylibrespot-java==0.1.1
pylitejet==0.5.0
# homeassistant.components.litterrobot
pylitterbot==2023.4.0
pylitterbot==2023.4.2
# homeassistant.components.lutron_caseta
pylutron-caseta==0.18.1
@ -1579,7 +1579,7 @@ pytrafikverket==0.3.3
pyudev==0.23.2
# homeassistant.components.unifiprotect
pyunifiprotect==4.10.2
pyunifiprotect==4.10.3
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@ -1630,7 +1630,7 @@ radiotherm==2.1.0
rapt-ble==0.1.1
# homeassistant.components.rainmachine
regenmaschine==2023.05.1
regenmaschine==2023.06.0
# homeassistant.components.renault
renault-api==0.1.13
@ -1953,7 +1953,7 @@ xiaomi-ble==0.17.2
xknx==2.10.0
# homeassistant.components.knx
xknxproject==3.1.0
xknxproject==3.1.1
# homeassistant.components.bluesound
# homeassistant.components.fritz
@ -1968,7 +1968,7 @@ yalesmartalarmclient==0.3.9
# homeassistant.components.august
# homeassistant.components.yalexs_ble
yalexs-ble==2.1.17
yalexs-ble==2.1.18
# homeassistant.components.august
yalexs==1.5.1

View File

@ -26,6 +26,7 @@ MOCK_STATUS: Final = OrderedDict(
("LOADPCT", "14.0 Percent"),
("BCHARGE", "100.0 Percent"),
("TIMELEFT", "51.0 Minutes"),
("NOMAPNT", "60.0 VA"),
("ITEMP", "34.6 C Internal"),
("MBATTCHG", "5 Percent"),
("MINTIMEL", "3 Minutes"),
@ -35,6 +36,7 @@ MOCK_STATUS: Final = OrderedDict(
("HITRANS", "139.0 Volts"),
("ALARMDEL", "30 Seconds"),
("BATTV", "13.7 Volts"),
("OUTCURNT", "0.88 Amps"),
("LASTXFER", "Automatic or explicit self test"),
("NUMXFERS", "1"),
("XONBATT", "1970-01-01 00:00:00 0000"),
@ -74,7 +76,7 @@ MOCK_MINIMAL_STATUS: Final = OrderedDict(
)
async def init_integration(
async def async_init_integration(
hass: HomeAssistant, host: str = "test", status=None
) -> MockConfigEntry:
"""Set up the APC UPS Daemon integration in HomeAssistant."""
@ -95,7 +97,7 @@ async def init_integration(
with patch("apcaccess.status.parse", return_value=status), patch(
"apcaccess.status.get", return_value=b""
):
await hass.config_entries.async_setup(entry.entry_id)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry

View File

@ -2,12 +2,12 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import MOCK_STATUS, init_integration
from . import MOCK_STATUS, async_init_integration
async def test_binary_sensor(hass: HomeAssistant) -> None:
"""Test states of binary sensor."""
await init_integration(hass, status=MOCK_STATUS)
await async_init_integration(hass, status=MOCK_STATUS)
registry = er.async_get(hass)
state = hass.states.get("binary_sensor.ups_online_status")
@ -22,7 +22,7 @@ async def test_no_binary_sensor(hass: HomeAssistant) -> None:
"""Test binary sensor when STATFLAG is not available."""
status = MOCK_STATUS.copy()
status.pop("STATFLAG")
await init_integration(hass, status=status)
await async_init_integration(hass, status=status)
state = hass.states.get("binary_sensor.ups_online_status")
assert state is None

View File

@ -9,7 +9,7 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, init_integration
from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration
from tests.common import MockConfigEntry
@ -19,7 +19,7 @@ async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> No
"""Test a successful setup entry."""
# Minimal status does not contain "SERIALNO" field, which is used to determine the
# unique ID of this integration. But, the integration should work fine without it.
await init_integration(hass, status=status)
await async_init_integration(hass, status=status)
# Verify successful setup by querying the status sensor.
state = hass.states.get("binary_sensor.ups_online_status")
@ -34,8 +34,8 @@ async def test_multiple_integrations(hass: HomeAssistant) -> None:
status1 = MOCK_STATUS | {"LOADPCT": "15.0 Percent", "SERIALNO": "XXXXX1"}
status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"}
entries = (
await init_integration(hass, host="test1", status=status1),
await init_integration(hass, host="test2", status=status2),
await async_init_integration(hass, host="test1", status=status1),
await async_init_integration(hass, host="test2", status=status2),
)
assert len(hass.config_entries.async_entries(DOMAIN)) == 2
@ -70,8 +70,8 @@ async def test_unload_remove(hass: HomeAssistant) -> None:
"""Test successful unload of entry."""
# Load two integrations from two mock hosts.
entries = (
await init_integration(hass, host="test1", status=MOCK_STATUS),
await init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS),
await async_init_integration(hass, host="test1", status=MOCK_STATUS),
await async_init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS),
)
# Assert they are loaded.

View File

@ -16,12 +16,12 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import MOCK_STATUS, init_integration
from . import MOCK_STATUS, async_init_integration
async def test_sensor(hass: HomeAssistant) -> None:
"""Test states of sensor."""
await init_integration(hass, status=MOCK_STATUS)
await async_init_integration(hass, status=MOCK_STATUS)
registry = er.async_get(hass)
# Test a representative string sensor.
@ -89,7 +89,7 @@ async def test_sensor(hass: HomeAssistant) -> None:
async def test_sensor_disabled(hass: HomeAssistant) -> None:
"""Test sensor disabled by default."""
await init_integration(hass)
await async_init_integration(hass)
registry = er.async_get(hass)
# Test a representative integration-disabled sensor.

View File

@ -1,6 +1,7 @@
cover:
- platform: command_line
covers:
from_yaml:
command_state: "echo closed"
value_template: "{{ value }}"
command_line:
- "binary_sensor":
"name": "Test"
"command": "echo 1"
"payload_on": "1"
"payload_off": "0"
"command_timeout": 15

View File

@ -12,7 +12,11 @@ from homeassistant import setup
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.command_line.binary_sensor import CommandBinarySensor
from homeassistant.components.command_line.const import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir
@ -252,3 +256,56 @@ async def test_updating_to_often(
)
await asyncio.sleep(0.2)
async def test_updating_manually(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling manual updating using homeassistant udate_entity service."""
await setup.async_setup_component(hass, HA_DOMAIN, {})
called = []
class MockCommandBinarySensor(CommandBinarySensor):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.binary_sensor.CommandBinarySensor",
side_effect=MockCommandBinarySensor,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"binary_sensor": {
"name": "Test",
"command": "echo 1",
"payload_on": "1",
"payload_off": "0",
"scan_interval": 10,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 1
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: ["binary_sensor.test"]},
blocking=True,
)
await hass.async_block_till_done()
assert len(called) == 2
await asyncio.sleep(0.2)

View File

@ -9,15 +9,18 @@ from unittest.mock import patch
import pytest
from homeassistant import config as hass_config, setup
from homeassistant import setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.cover import CommandCover
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL
from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER,
SERVICE_RELOAD,
SERVICE_STOP_COVER,
)
from homeassistant.core import HomeAssistant
@ -25,7 +28,7 @@ from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, get_fixture_path
from tests.common import async_fire_time_changed
async def test_no_covers_platform_yaml(
@ -210,45 +213,6 @@ async def test_state_value(hass: HomeAssistant) -> None:
assert entity_state.state == "closed"
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"cover": {
"command_state": "echo open",
"value_template": "{{ value }}",
"name": "Test",
}
}
]
}
],
)
async def test_reload(hass: HomeAssistant, load_yaml_integration: None) -> None:
"""Verify we can reload command_line covers."""
entity_state = hass.states.get("cover.test")
assert entity_state
assert entity_state.state == "unknown"
yaml_path = get_fixture_path("configuration.yaml", "command_line")
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(
"command_line",
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert not hass.states.get("cover.test")
assert hass.states.get("cover.from_yaml")
@pytest.mark.parametrize(
"get_config",
[
@ -378,3 +342,57 @@ async def test_updating_to_often(
)
await asyncio.sleep(0.2)
async def test_updating_manually(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling manual updating using homeassistant udate_entity service."""
await setup.async_setup_component(hass, HA_DOMAIN, {})
called = []
class MockCommandCover(CommandCover):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.cover.CommandCover",
side_effect=MockCommandCover,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"cover": {
"command_state": "echo 1",
"value_template": "{{ value }}",
"name": "Test",
"scan_interval": 10,
}
}
]
},
)
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10))
await hass.async_block_till_done()
assert len(called) == 1
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: ["cover.test"]},
blocking=True,
)
await hass.async_block_till_done()
assert len(called) == 2
await asyncio.sleep(0.2)

View File

@ -2,12 +2,17 @@
from __future__ import annotations
from datetime import timedelta
from unittest.mock import patch
from homeassistant.const import STATE_ON, STATE_OPEN
import pytest
from homeassistant import config as hass_config
from homeassistant.components.command_line.const import DOMAIN
from homeassistant.const import SERVICE_RELOAD, STATE_ON, STATE_OPEN
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
from tests.common import async_fire_time_changed, get_fixture_path
async def test_setup_config(hass: HomeAssistant, load_yaml_integration: None) -> None:
@ -25,3 +30,55 @@ async def test_setup_config(hass: HomeAssistant, load_yaml_integration: None) ->
assert state_sensor.state == "5"
assert state_cover.state == STATE_OPEN
assert state_switch.state == STATE_ON
async def test_reload_service(
hass: HomeAssistant, load_yaml_integration: None, caplog: pytest.LogCaptureFixture
) -> None:
"""Test reload serviice."""
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10))
await hass.async_block_till_done()
state_binary_sensor = hass.states.get("binary_sensor.test")
state_sensor = hass.states.get("sensor.test")
assert state_binary_sensor.state == STATE_ON
assert state_sensor.state == "5"
caplog.clear()
yaml_path = get_fixture_path("configuration.yaml", "command_line")
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
assert "Loading config" in caplog.text
state_binary_sensor = hass.states.get("binary_sensor.test")
state_sensor = hass.states.get("sensor.test")
assert state_binary_sensor.state == STATE_ON
assert not state_sensor
caplog.clear()
yaml_path = get_fixture_path("configuration_empty.yaml", "command_line")
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
state_binary_sensor = hass.states.get("binary_sensor.test")
state_sensor = hass.states.get("sensor.test")
assert not state_binary_sensor
assert not state_sensor
assert "Loading config" not in caplog.text

View File

@ -11,7 +11,12 @@ import pytest
from homeassistant import setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.sensor import CommandSensor
from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir
@ -586,3 +591,54 @@ async def test_updating_to_often(
)
await asyncio.sleep(0.2)
async def test_updating_manually(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling manual updating using homeassistant udate_entity service."""
await setup.async_setup_component(hass, HA_DOMAIN, {})
called = []
class MockCommandSensor(CommandSensor):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.sensor.CommandSensor",
side_effect=MockCommandSensor,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"sensor": {
"name": "Test",
"command": "echo 1",
"scan_interval": 10,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 1
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: ["sensor.test"]},
blocking=True,
)
await hass.async_block_till_done()
assert len(called) == 2
await asyncio.sleep(0.2)

View File

@ -14,6 +14,10 @@ import pytest
from homeassistant import setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.switch import CommandSwitch
from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SCAN_INTERVAL
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -696,3 +700,58 @@ async def test_updating_to_often(
)
await asyncio.sleep(0.2)
async def test_updating_manually(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling manual updating using homeassistant udate_entity service."""
await setup.async_setup_component(hass, HA_DOMAIN, {})
called = []
class MockCommandSwitch(CommandSwitch):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.switch.CommandSwitch",
side_effect=MockCommandSwitch,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"switch": {
"command_state": "echo 1",
"command_on": "echo 2",
"command_off": "echo 3",
"name": "Test",
"scan_interval": 10,
}
}
]
},
)
await hass.async_block_till_done()
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10))
await hass.async_block_till_done()
assert len(called) == 1
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: ["switch.test"]},
blocking=True,
)
await hass.async_block_till_done()
assert len(called) == 2
await asyncio.sleep(0.2)

View File

@ -151,11 +151,11 @@ class MockDevices:
for flag in operating_flags:
value = operating_flags[flag]
if device.operating_flags.get(flag):
device.operating_flags[flag].load(value)
device.operating_flags[flag].set_value(value)
for flag in properties:
value = properties[flag]
if device.properties.get(flag):
device.properties[flag].load(value)
device.properties[flag].set_value(value)
async def async_add_device(self, address=None, multiple=False):
"""Mock the async_add_device method."""

View File

@ -119,7 +119,7 @@ async def test_get_read_only_properties(
mock_read_only = ExtendedProperty(
"44.44.44", "mock_read_only", bool, is_read_only=True
)
mock_read_only.load(False)
mock_read_only.set_value(False)
ws_client, devices = await _setup(
hass, hass_ws_client, "44.44.44", iolinc_properties_data
@ -368,7 +368,7 @@ async def test_change_float_property(
)
device = devices["44.44.44"]
delay_prop = device.configuration[MOMENTARY_DELAY]
delay_prop.load(0)
delay_prop.set_value(0)
with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json(
{

View File

@ -1,6 +1,6 @@
"""Tests for Shelly coordinator."""
from datetime import timedelta
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, patch
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
@ -335,6 +335,59 @@ async def test_rpc_reload_on_cfg_change(
assert hass.states.get("switch.test_switch_0") is None
async def test_rpc_reload_with_invalid_auth(
hass: HomeAssistant, mock_rpc_device, monkeypatch
) -> None:
"""Test RPC when InvalidAuthError is raising during config entry reload."""
with patch(
"homeassistant.components.shelly.coordinator.async_stop_scanner",
side_effect=[None, InvalidAuthError, None],
):
entry = await init_integration(hass, 2)
inject_rpc_device_event(
monkeypatch,
mock_rpc_device,
{
"events": [
{
"data": [],
"event": "config_changed",
"id": 1,
"ts": 1668522399.2,
},
{
"data": [],
"id": 2,
"ts": 1668522399.2,
},
],
"ts": 1668522399.2,
},
)
await hass.async_block_till_done()
# Move time to generate reconnect
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(seconds=RPC_RECONNECT_INTERVAL)
)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
flow = flows[0]
assert flow.get("step_id") == "reauth_confirm"
assert flow.get("handler") == DOMAIN
assert "context" in flow
assert flow["context"].get("source") == SOURCE_REAUTH
assert flow["context"].get("entry_id") == entry.entry_id
async def test_rpc_click_event(
hass: HomeAssistant, mock_rpc_device, events, monkeypatch
) -> None:

View File

@ -0,0 +1,9 @@
{
"kind": "youtube#channelListResponse",
"etag": "8HTiiXpKCq-GJvDVOd88e5o_KGc",
"pageInfo": {
"totalResults": 0,
"resultsPerPage": 5
},
"items": []
}

View File

@ -83,6 +83,46 @@ async def test_full_flow(
assert result["options"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]}
async def test_flow_abort_without_channel(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
current_request_with_host: None,
) -> None:
"""Check abort flow if user has no channel."""
result = await hass.config_entries.flow.async_init(
"youtube", context={"source": config_entries.SOURCE_USER}
)
state = config_entry_oauth2_flow._encode_jwt(
hass,
{
"flow_id": result["flow_id"],
"redirect_uri": "https://example.com/auth/external/callback",
},
)
assert result["url"] == (
f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope={'+'.join(SCOPES)}"
"&access_type=offline&prompt=consent"
)
client = await hass_client_no_auth()
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
assert resp.status == 200
assert resp.headers["content-type"] == "text/html; charset=utf-8"
service = MockService(channel_fixture="youtube/get_no_channel.json")
with patch(
"homeassistant.components.youtube.async_setup_entry", return_value=True
), patch("homeassistant.components.youtube.api.build", return_value=service), patch(
"homeassistant.components.youtube.config_flow.build", return_value=service
):
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "no_channel"
async def test_flow_http_error(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,

View File

@ -4,15 +4,17 @@ from __future__ import annotations
from binascii import unhexlify
from copy import deepcopy
from typing import TYPE_CHECKING
from unittest.mock import ANY, AsyncMock, call, patch
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
import pytest
import voluptuous as vol
import zigpy.backups
import zigpy.profiles.zha
import zigpy.types
from zigpy.types.named import EUI64
import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.security as security
import zigpy.zdo.types as zdo_types
from homeassistant.components.websocket_api import const
from homeassistant.components.zha import DOMAIN
@ -26,6 +28,8 @@ from homeassistant.components.zha.core.const import (
ATTR_MODEL,
ATTR_NEIGHBORS,
ATTR_QUIRK_APPLIED,
ATTR_TYPE,
BINDINGS,
CLUSTER_TYPE_IN,
EZSP_OVERWRITE_EUI64,
GROUP_ID,
@ -37,6 +41,7 @@ from homeassistant.components.zha.websocket_api import (
ATTR_INSTALL_CODE,
ATTR_QR_CODE,
ATTR_SOURCE_IEEE,
ATTR_TARGET_IEEE,
ID,
SERVICE_PERMIT,
TYPE,
@ -884,3 +889,90 @@ async def test_websocket_change_channel(
assert msg["success"]
change_channel_mock.mock_calls == [call(ANY, new_channel)]
@pytest.mark.parametrize(
"operation",
[("bind", zdo_types.ZDOCmd.Bind_req), ("unbind", zdo_types.ZDOCmd.Unbind_req)],
)
async def test_websocket_bind_unbind_devices(
operation: tuple[str, zdo_types.ZDOCmd],
app_controller: ControllerApplication,
zha_client,
) -> None:
"""Test websocket API for binding and unbinding devices to devices."""
command_type, req = operation
with patch(
"homeassistant.components.zha.websocket_api.async_binding_operation",
autospec=True,
) as binding_operation_mock:
await zha_client.send_json(
{
ID: 27,
TYPE: f"zha/devices/{command_type}",
ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
ATTR_TARGET_IEEE: IEEE_GROUPABLE_DEVICE,
}
)
msg = await zha_client.receive_json()
assert msg["id"] == 27
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
assert binding_operation_mock.mock_calls == [
call(
ANY,
EUI64.convert(IEEE_SWITCH_DEVICE),
EUI64.convert(IEEE_GROUPABLE_DEVICE),
req,
)
]
@pytest.mark.parametrize("command_type", ["bind", "unbind"])
async def test_websocket_bind_unbind_group(
command_type: str,
app_controller: ControllerApplication,
zha_client,
) -> None:
"""Test websocket API for binding and unbinding devices to groups."""
test_group_id = 0x0001
gateway_mock = MagicMock()
with patch(
"homeassistant.components.zha.websocket_api.get_gateway",
return_value=gateway_mock,
):
device_mock = MagicMock()
bind_mock = AsyncMock()
unbind_mock = AsyncMock()
device_mock.async_bind_to_group = bind_mock
device_mock.async_unbind_from_group = unbind_mock
gateway_mock.get_device = MagicMock()
gateway_mock.get_device.return_value = device_mock
await zha_client.send_json(
{
ID: 27,
TYPE: f"zha/groups/{command_type}",
ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE,
GROUP_ID: test_group_id,
BINDINGS: [
{
ATTR_ENDPOINT_ID: 1,
ID: 6,
ATTR_NAME: "OnOff",
ATTR_TYPE: "out",
},
],
}
)
msg = await zha_client.receive_json()
assert msg["id"] == 27
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
if command_type == "bind":
assert bind_mock.mock_calls == [call(test_group_id, ANY)]
elif command_type == "unbind":
assert unbind_mock.mock_calls == [call(test_group_id, ANY)]

View File

@ -196,6 +196,21 @@ async def test_actions(
"value": 1,
},
},
{
"trigger": {
"platform": "event",
"event_type": "test_event_set_config_parameter_no_endpoint",
},
"action": {
"domain": DOMAIN,
"type": "set_config_parameter",
"device_id": device.id,
"parameter": 1,
"bitmask": None,
"subtype": "3 (Beeper)",
"value": 1,
},
},
]
},
)
@ -245,6 +260,18 @@ async def test_actions(
assert args[1] == 1
assert args[2] == 1
with patch(
"homeassistant.components.zwave_js.services.async_set_config_parameter"
) as mock_call:
hass.bus.async_fire("test_event_set_config_parameter_no_endpoint")
await hass.async_block_till_done()
mock_call.assert_called_once()
args = mock_call.call_args_list[0][0]
assert len(args) == 3
assert args[0].node_id == 13
assert args[1] == 1
assert args[2] == 1
async def test_actions_multiple_calls(
hass: HomeAssistant,

View File

@ -1109,3 +1109,101 @@ def test_get_trigger_platform_failure() -> None:
"""Test _get_trigger_platform."""
with pytest.raises(ValueError):
_get_trigger_platform({CONF_PLATFORM: "zwave_js.invalid"})
async def test_server_reconnect_event(
hass: HomeAssistant, client, lock_schlage_be469, integration
) -> None:
"""Test that when we reconnect to server, event triggers reattach."""
trigger_type = f"{DOMAIN}.event"
node: Node = lock_schlage_be469
dev_reg = async_get_dev_reg(hass)
device = dev_reg.async_get_device(
{get_device_id(client.driver, lock_schlage_be469)}
)
assert device
event_name = "interview stage completed"
original_len = len(node._listeners.get(event_name, []))
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": trigger_type,
"entity_id": SCHLAGE_BE469_LOCK_ENTITY,
"event_source": "node",
"event": event_name,
},
"action": {
"event": "blah",
},
},
]
},
)
assert len(node._listeners.get(event_name, [])) == original_len + 1
old_listener = node._listeners.get(event_name, [])[original_len]
await hass.config_entries.async_reload(integration.entry_id)
await hass.async_block_till_done()
# Make sure there is still a listener added for the trigger
assert len(node._listeners.get(event_name, [])) == original_len + 1
# Make sure the old listener was removed
assert old_listener not in node._listeners.get(event_name, [])
async def test_server_reconnect_value_updated(
hass: HomeAssistant, client, lock_schlage_be469, integration
) -> None:
"""Test that when we reconnect to server, value_updated triggers reattach."""
trigger_type = f"{DOMAIN}.value_updated"
node: Node = lock_schlage_be469
dev_reg = async_get_dev_reg(hass)
device = dev_reg.async_get_device(
{get_device_id(client.driver, lock_schlage_be469)}
)
assert device
event_name = "value updated"
original_len = len(node._listeners.get(event_name, []))
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": trigger_type,
"entity_id": SCHLAGE_BE469_LOCK_ENTITY,
"command_class": CommandClass.DOOR_LOCK.value,
"property": "latchStatus",
},
"action": {
"event": "no_value_filter",
},
},
]
},
)
assert len(node._listeners.get(event_name, [])) == original_len + 1
old_listener = node._listeners.get(event_name, [])[original_len]
await hass.config_entries.async_reload(integration.entry_id)
await hass.async_block_till_done()
# Make sure there is still a listener added for the trigger
assert len(node._listeners.get(event_name, [])) == original_len + 1
# Make sure the old listener was removed
assert old_listener not in node._listeners.get(event_name, [])