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 image: homeassistant/{arch}-homeassistant
shadow_repository: ghcr.io/home-assistant shadow_repository: ghcr.io/home-assistant
build_from: build_from:
aarch64: ghcr.io/home-assistant/aarch64-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.05.0 armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.05.0 armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.05.0 amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.05.0 i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.0
codenotary: codenotary:
signer: notary@home-assistant.io signer: notary@home-assistant.io
base_image: notary@home-assistant.io base_image: notary@home-assistant.io

View File

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

View File

@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august", "documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"], "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_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
SERVICE_RELOAD,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv 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 homeassistant.helpers.typing import ConfigType
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN 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: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Command Line from yaml config.""" """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: if not command_line_config:
return True return
_LOGGER.debug("Full config loaded: %s", command_line_config) _LOGGER.debug("Full config loaded: %s", command_line_config)
load_coroutines: list[Coroutine[Any, Any, None]] = [] load_coroutines: list[Coroutine[Any, Any, None]] = []
platforms: list[Platform] = [] platforms: list[Platform] = []
reload_configs: list[tuple] = []
for platform_config in command_line_config: for platform_config in command_line_config:
for platform, _config in platform_config.items(): for platform, _config in platform_config.items():
if (mapped_platform := PLATFORM_MAPPING[platform]) not in platforms: 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_config,
PLATFORM_MAPPING[platform], PLATFORM_MAPPING[platform],
) )
reload_configs.append((PLATFORM_MAPPING[platform], _config))
load_coroutines.append( load_coroutines.append(
discovery.async_load_platform( discovery.async_load_platform(
hass, 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: if load_coroutines:
_LOGGER.debug("Loading platforms: %s", platforms) _LOGGER.debug("Loading platforms: %s", platforms)
await asyncio.gather(*load_coroutines) 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.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 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 .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .sensor import CommandSensorData from .sensor import CommandSensorData
@ -183,3 +184,10 @@ class CommandBinarySensor(BinarySensorEntity):
self._attr_is_on = False self._attr_is_on = False
self.async_write_ha_state() 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.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 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 .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log from .utils import call_shell_with_timeout, check_output_or_log
@ -220,6 +220,13 @@ class CommandCover(CoverEntity):
self._state = int(payload) self._state = int(payload)
await self.async_update_ha_state(True) 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: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover.""" """Open the cover."""
await self.hass.async_add_executor_job(self._move_cover, self._command_open) 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.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 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 .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import check_output_or_log from .utils import check_output_or_log
@ -200,6 +201,13 @@ class CommandSensor(SensorEntity):
self.async_write_ha_state() 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: class CommandSensorData:
"""The class for handling the data retrieval.""" """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 import Template
from homeassistant.helpers.template_entity import ManualTriggerEntity from homeassistant.helpers.template_entity import ManualTriggerEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 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 .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log from .utils import call_shell_with_timeout, check_output_or_log
@ -240,6 +240,13 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
self._process_manual_data(payload) self._process_manual_data(payload)
await self.async_update_ha_state(True) 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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
if await self._switch(self._command_on) and not self._command_state: if await self._switch(self._command_on) and not self._command_state:

View File

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

View File

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

View File

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

View File

@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/elkm1", "documentation": "https://www.home-assistant.io/integrations/elkm1",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["elkm1_lib"], "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: ) -> None:
"""Register a multipan platform.""" """Register a multipan platform."""
self._platforms[integration_domain] = 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 return
new_channel = await platform.async_get_channel(hass) if channel is None:
if new_channel is None:
return return
_LOGGER.info( _LOGGER.info(
"Setting multipan channel to %s (source: '%s')", "Setting multipan channel to %s (source: '%s')",
new_channel, channel,
integration_domain, integration_domain,
) )
self.async_set_channel(new_channel) self.async_set_channel(channel)
async def async_change_channel( async def async_change_channel(
self, channel: int, delay: float self, channel: int, delay: float

View File

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

View File

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

View File

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

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "documentation": "https://www.home-assistant.io/integrations/keyboard_remote",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aionotify", "evdev"], "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", "quality_scale": "platinum",
"requirements": [ "requirements": [
"xknx==2.10.0", "xknx==2.10.0",
"xknxproject==3.1.0", "xknxproject==3.1.1",
"knx_frontend==2023.5.31.141540" "knx-frontend==2023.6.9.195839"
] ]
} }

View File

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

View File

@ -12,5 +12,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pylitterbot"], "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", "documentation": "https://www.home-assistant.io/integrations/noaa_tides",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["noaa_coops"], "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", "documentation": "https://www.home-assistant.io/integrations/opple",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyoppleio"], "loggers": ["pyoppleio"],
"requirements": ["pyoppleio==1.0.5"] "requirements": ["pyoppleio-legacy==1.0.8"]
} }

View File

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

View File

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

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/russound_rio", "documentation": "https://www.home-assistant.io/integrations/russound_rio",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["russound_rio"], "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: async def shutdown(self) -> None:
"""Shutdown the coordinator.""" """Shutdown the coordinator."""
if self.device.connected: 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.device.shutdown()
await self._async_disconnected() await self._async_disconnected()

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/sisyphus", "documentation": "https://www.home-assistant.io/integrations/sisyphus",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["sisyphus_control"], "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", "iot_class": "local_push",
"loggers": ["pyunifiprotect", "unifi_discovery"], "loggers": ["pyunifiprotect", "unifi_discovery"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pyunifiprotect==4.10.2", "unifi-discovery==1.1.7"], "requirements": ["pyunifiprotect==4.10.3", "unifi-discovery==1.1.7"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -275,9 +275,7 @@ async def async_setup_entry(
if not entity_method: if not entity_method:
continue continue
await entity_method(**params) await entity_method(**params)
update_tasks.append( update_tasks.append(asyncio.create_task(entity.async_update_ha_state(True)))
hass.async_create_task(entity.async_update_ha_state(True))
)
if update_tasks: if update_tasks:
await asyncio.wait(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"]): if not hasattr(target_device, method["method"]):
continue continue
await getattr(target_device, method["method"])(**params) 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: if update_tasks:
await asyncio.wait(update_tasks) await asyncio.wait(update_tasks)

View File

@ -441,7 +441,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity):
return await self._try_command( return await self._try_command(
"Setting delay off miio device failed.", "Setting delay off miio device failed.",
self._device.delay_off, self._device.delay_off,
delay_off_countdown * 60, delay_off_countdown,
) )
async def async_set_led_brightness_level(self, level: int) -> bool: 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"]): if not hasattr(device, method["method"]):
continue continue
await getattr(device, method["method"])(**params) 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: if update_tasks:
await asyncio.wait(update_tasks) await asyncio.wait(update_tasks)

View File

@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"iot_class": "local_push", "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, 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: async def get_resource(hass: HomeAssistant, token: str) -> Resource:
@ -99,6 +105,11 @@ class OAuth2FlowHandler(
response = await self.hass.async_add_executor_job( response = await self.hass.async_add_executor_job(
own_channel_request.execute 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] own_channel = response["items"][0]
except HttpError as ex: except HttpError as ex:
error = ex.reason error = ex.reason

View File

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

View File

@ -2,6 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"access_not_configured": "Please read the below message we got from Google:\n\n{message}", "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%]", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"

View File

@ -907,6 +907,7 @@ async def websocket_bind_devices(
ATTR_TARGET_IEEE, ATTR_TARGET_IEEE,
target_ieee, target_ieee,
) )
connection.send_result(msg[ID])
@websocket_api.require_admin @websocket_api.require_admin
@ -935,6 +936,7 @@ async def websocket_unbind_devices(
ATTR_TARGET_IEEE, ATTR_TARGET_IEEE,
target_ieee, target_ieee,
) )
connection.send_result(msg[ID])
@websocket_api.require_admin @websocket_api.require_admin
@ -951,13 +953,14 @@ async def websocket_bind_group(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None: ) -> None:
"""Directly bind a device to a group.""" """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] source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE]
group_id: int = msg[GROUP_ID] group_id: int = msg[GROUP_ID]
bindings: list[ClusterBinding] = msg[BINDINGS] bindings: list[ClusterBinding] = msg[BINDINGS]
source_device = zha_gateway.get_device(source_ieee) source_device = zha_gateway.get_device(source_ieee)
assert source_device assert source_device
await source_device.async_bind_to_group(group_id, bindings) await source_device.async_bind_to_group(group_id, bindings)
connection.send_result(msg[ID])
@websocket_api.require_admin @websocket_api.require_admin
@ -974,13 +977,19 @@ async def websocket_unbind_group(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None: ) -> None:
"""Unbind a device from a group.""" """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] source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE]
group_id: int = msg[GROUP_ID] group_id: int = msg[GROUP_ID]
bindings: list[ClusterBinding] = msg[BINDINGS] bindings: list[ClusterBinding] = msg[BINDINGS]
source_device = zha_gateway.get_device(source_ieee) source_device = zha_gateway.get_device(source_ieee)
assert source_device assert source_device
await source_device.async_unbind_from_group(group_id, bindings) 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( async def async_binding_operation(

View File

@ -209,6 +209,9 @@ async def start_client(
LOGGER.info("Connection to Zwave JS Server initialized") LOGGER.info("Connection to Zwave JS Server initialized")
assert client.driver assert client.driver
async_dispatcher_send(
hass, f"{DOMAIN}_{client.driver.controller.home_id}_connected_to_server"
)
await driver_events.setup(client.driver) 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( SET_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{ {
vol.Required(CONF_TYPE): SERVICE_SET_CONFIG_PARAMETER, 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): vol.Any(int, str),
vol.Required(ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(None, int, str), vol.Required(ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(None, int, str),
vol.Required(ATTR_VALUE): vol.Coerce(int), 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_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]),
vol.Required(ATTR_PROPERTY): vol.Any(int, str), vol.Required(ATTR_PROPERTY): vol.Any(int, str),
vol.Optional(ATTR_PROPERTY_KEY): vol.Any(None, vol.Coerce(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_FROM): VALUE_SCHEMA,
vol.Optional(ATTR_TO): VALUE_SCHEMA, vol.Optional(ATTR_TO): VALUE_SCHEMA,
} }

View File

@ -1,18 +1,20 @@
"""Offer Z-Wave JS event listening automation trigger.""" """Offer Z-Wave JS event listening automation trigger."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
import functools import functools
from pydantic import ValidationError from pydantic import ValidationError
import voluptuous as vol import voluptuous as vol
from zwave_js_server.client import Client from zwave_js_server.client import Client
from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP 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 zwave_js_server.model.node import NODE_EVENT_MODEL_MAP
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -150,7 +152,7 @@ async def async_attach_trigger(
event_name = config[ATTR_EVENT] event_name = config[ATTR_EVENT]
event_data_filter = config.get(ATTR_EVENT_DATA, {}) event_data_filter = config.get(ATTR_EVENT_DATA, {})
unsubs = [] unsubs: list[Callable] = []
job = HassJob(action) job = HassJob(action)
trigger_data = trigger_info["trigger_data"] trigger_data = trigger_info["trigger_data"]
@ -199,26 +201,6 @@ async def async_attach_trigger(
hass.async_run_hass_job(job, {"trigger": payload}) 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 @callback
def async_remove() -> None: def async_remove() -> None:
"""Remove state listeners async.""" """Remove state listeners async."""
@ -226,4 +208,45 @@ async def async_attach_trigger(
unsub() unsub()
unsubs.clear() 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 return async_remove

View File

@ -1,15 +1,18 @@
"""Offer Z-Wave JS value updated listening automation trigger.""" """Offer Z-Wave JS value updated listening automation trigger."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
import functools import functools
import voluptuous as vol import voluptuous as vol
from zwave_js_server.const import CommandClass 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 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.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -99,7 +102,7 @@ async def async_attach_trigger(
property_ = config[ATTR_PROPERTY] property_ = config[ATTR_PROPERTY]
endpoint = config.get(ATTR_ENDPOINT) endpoint = config.get(ATTR_ENDPOINT)
property_key = config.get(ATTR_PROPERTY_KEY) property_key = config.get(ATTR_PROPERTY_KEY)
unsubs = [] unsubs: list[Callable] = []
job = HassJob(action) job = HassJob(action)
trigger_data = trigger_info["trigger_data"] trigger_data = trigger_info["trigger_data"]
@ -153,29 +156,11 @@ async def async_attach_trigger(
ATTR_PREVIOUS_VALUE_RAW: prev_value_raw, ATTR_PREVIOUS_VALUE_RAW: prev_value_raw,
ATTR_CURRENT_VALUE: curr_value, ATTR_CURRENT_VALUE: curr_value,
ATTR_CURRENT_VALUE_RAW: curr_value_raw, 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}) 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 @callback
def async_remove() -> None: def async_remove() -> None:
"""Remove state listeners async.""" """Remove state listeners async."""
@ -183,4 +168,40 @@ async def async_attach_trigger(
unsub() unsub()
unsubs.clear() 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 return async_remove

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,12 +2,12 @@
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er 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: async def test_binary_sensor(hass: HomeAssistant) -> None:
"""Test states of binary sensor.""" """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) registry = er.async_get(hass)
state = hass.states.get("binary_sensor.ups_online_status") 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.""" """Test binary sensor when STATFLAG is not available."""
status = MOCK_STATUS.copy() status = MOCK_STATUS.copy()
status.pop("STATFLAG") 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") state = hass.states.get("binary_sensor.ups_online_status")
assert state is None 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.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant 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 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.""" """Test a successful setup entry."""
# Minimal status does not contain "SERIALNO" field, which is used to determine the # 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. # 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. # Verify successful setup by querying the status sensor.
state = hass.states.get("binary_sensor.ups_online_status") 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"} status1 = MOCK_STATUS | {"LOADPCT": "15.0 Percent", "SERIALNO": "XXXXX1"}
status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"} status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"}
entries = ( entries = (
await init_integration(hass, host="test1", status=status1), await async_init_integration(hass, host="test1", status=status1),
await init_integration(hass, host="test2", status=status2), await async_init_integration(hass, host="test2", status=status2),
) )
assert len(hass.config_entries.async_entries(DOMAIN)) == 2 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.""" """Test successful unload of entry."""
# Load two integrations from two mock hosts. # Load two integrations from two mock hosts.
entries = ( entries = (
await init_integration(hass, host="test1", status=MOCK_STATUS), await async_init_integration(hass, host="test1", status=MOCK_STATUS),
await init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS), await async_init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS),
) )
# Assert they are loaded. # Assert they are loaded.

View File

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

View File

@ -1,6 +1,7 @@
cover: command_line:
- platform: command_line - "binary_sensor":
covers: "name": "Test"
from_yaml: "command": "echo 1"
command_state: "echo closed" "payload_on": "1"
value_template: "{{ value }}" "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.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.command_line.binary_sensor import CommandBinarySensor from homeassistant.components.command_line.binary_sensor import CommandBinarySensor
from homeassistant.components.command_line.const import DOMAIN 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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir import homeassistant.helpers.issue_registry as ir
@ -252,3 +256,56 @@ async def test_updating_to_often(
) )
await asyncio.sleep(0.2) 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 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 import DOMAIN
from homeassistant.components.command_line.cover import CommandCover from homeassistant.components.command_line.cover import CommandCover
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL 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 ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER, SERVICE_OPEN_COVER,
SERVICE_RELOAD,
SERVICE_STOP_COVER, SERVICE_STOP_COVER,
) )
from homeassistant.core import HomeAssistant 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.helpers.issue_registry as ir
import homeassistant.util.dt as dt_util 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( async def test_no_covers_platform_yaml(
@ -210,45 +213,6 @@ async def test_state_value(hass: HomeAssistant) -> None:
assert entity_state.state == "closed" 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( @pytest.mark.parametrize(
"get_config", "get_config",
[ [
@ -378,3 +342,57 @@ async def test_updating_to_often(
) )
await asyncio.sleep(0.2) 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 __future__ import annotations
from datetime import timedelta 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 from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util 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: 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_sensor.state == "5"
assert state_cover.state == STATE_OPEN assert state_cover.state == STATE_OPEN
assert state_switch.state == STATE_ON 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 import setup
from homeassistant.components.command_line import DOMAIN from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.sensor import CommandSensor 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.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir import homeassistant.helpers.issue_registry as ir
@ -586,3 +591,54 @@ async def test_updating_to_often(
) )
await asyncio.sleep(0.2) 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 import setup
from homeassistant.components.command_line import DOMAIN from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.switch import CommandSwitch 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.components.switch import DOMAIN as SWITCH_DOMAIN, SCAN_INTERVAL
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -696,3 +700,58 @@ async def test_updating_to_often(
) )
await asyncio.sleep(0.2) 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: for flag in operating_flags:
value = operating_flags[flag] value = operating_flags[flag]
if device.operating_flags.get(flag): if device.operating_flags.get(flag):
device.operating_flags[flag].load(value) device.operating_flags[flag].set_value(value)
for flag in properties: for flag in properties:
value = properties[flag] value = properties[flag]
if device.properties.get(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): async def async_add_device(self, address=None, multiple=False):
"""Mock the async_add_device method.""" """Mock the async_add_device method."""

View File

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

View File

@ -1,6 +1,6 @@
"""Tests for Shelly coordinator.""" """Tests for Shelly coordinator."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import AsyncMock from unittest.mock import AsyncMock, patch
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError 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 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( async def test_rpc_click_event(
hass: HomeAssistant, mock_rpc_device, events, monkeypatch hass: HomeAssistant, mock_rpc_device, events, monkeypatch
) -> None: ) -> 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"]} 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( async def test_flow_http_error(
hass: HomeAssistant, hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator, hass_client_no_auth: ClientSessionGenerator,

View File

@ -4,15 +4,17 @@ from __future__ import annotations
from binascii import unhexlify from binascii import unhexlify
from copy import deepcopy from copy import deepcopy
from typing import TYPE_CHECKING 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 pytest
import voluptuous as vol import voluptuous as vol
import zigpy.backups import zigpy.backups
import zigpy.profiles.zha import zigpy.profiles.zha
import zigpy.types import zigpy.types
from zigpy.types.named import EUI64
import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.security as security import zigpy.zcl.clusters.security as security
import zigpy.zdo.types as zdo_types
from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api import const
from homeassistant.components.zha import DOMAIN from homeassistant.components.zha import DOMAIN
@ -26,6 +28,8 @@ from homeassistant.components.zha.core.const import (
ATTR_MODEL, ATTR_MODEL,
ATTR_NEIGHBORS, ATTR_NEIGHBORS,
ATTR_QUIRK_APPLIED, ATTR_QUIRK_APPLIED,
ATTR_TYPE,
BINDINGS,
CLUSTER_TYPE_IN, CLUSTER_TYPE_IN,
EZSP_OVERWRITE_EUI64, EZSP_OVERWRITE_EUI64,
GROUP_ID, GROUP_ID,
@ -37,6 +41,7 @@ from homeassistant.components.zha.websocket_api import (
ATTR_INSTALL_CODE, ATTR_INSTALL_CODE,
ATTR_QR_CODE, ATTR_QR_CODE,
ATTR_SOURCE_IEEE, ATTR_SOURCE_IEEE,
ATTR_TARGET_IEEE,
ID, ID,
SERVICE_PERMIT, SERVICE_PERMIT,
TYPE, TYPE,
@ -884,3 +889,90 @@ async def test_websocket_change_channel(
assert msg["success"] assert msg["success"]
change_channel_mock.mock_calls == [call(ANY, new_channel)] 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, "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[1] == 1
assert args[2] == 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( async def test_actions_multiple_calls(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -1109,3 +1109,101 @@ def test_get_trigger_platform_failure() -> None:
"""Test _get_trigger_platform.""" """Test _get_trigger_platform."""
with pytest.raises(ValueError): with pytest.raises(ValueError):
_get_trigger_platform({CONF_PLATFORM: "zwave_js.invalid"}) _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, [])