mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
2023.6.2 (#94621)
This commit is contained in:
commit
e5c5790768
10
build.yaml
10
build.yaml
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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:
|
||||
|
@ -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."]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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": [
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"requirements": ["pylitterbot==2023.4.0"]
|
||||
"requirements": ["pylitterbot==2023.4.2"]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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.",
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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%]"
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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:
|
||||
|
9
tests/components/youtube/fixtures/get_no_channel.json
Normal file
9
tests/components/youtube/fixtures/get_no_channel.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"kind": "youtube#channelListResponse",
|
||||
"etag": "8HTiiXpKCq-GJvDVOd88e5o_KGc",
|
||||
"pageInfo": {
|
||||
"totalResults": 0,
|
||||
"resultsPerPage": 5
|
||||
},
|
||||
"items": []
|
||||
}
|
@ -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,
|
||||
|
@ -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)]
|
||||
|
@ -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,
|
||||
|
@ -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, [])
|
||||
|
Loading…
x
Reference in New Issue
Block a user