mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Merge pull request #64317 from home-assistant/rc
This commit is contained in:
commit
7887f23824
@ -138,6 +138,17 @@ class AugustData(AugustSubscriberMixin):
|
||||
pubnub.subscribe(self.async_pubnub_message)
|
||||
self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub)
|
||||
|
||||
if self._locks_by_id:
|
||||
tasks = []
|
||||
for lock_id in self._locks_by_id:
|
||||
detail = self._device_detail_by_id[lock_id]
|
||||
tasks.append(
|
||||
self.async_status_async(
|
||||
lock_id, bool(detail.bridge and detail.bridge.hyper_bridge)
|
||||
)
|
||||
)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
@callback
|
||||
def async_pubnub_message(self, device_id, date_time, message):
|
||||
"""Process a pubnub message."""
|
||||
@ -245,13 +256,24 @@ class AugustData(AugustSubscriberMixin):
|
||||
device_id,
|
||||
)
|
||||
|
||||
async def async_lock_async(self, device_id):
|
||||
async def async_status_async(self, device_id, hyper_bridge):
|
||||
"""Request status of the the device but do not wait for a response since it will come via pubnub."""
|
||||
return await self._async_call_api_op_requires_bridge(
|
||||
device_id,
|
||||
self._api.async_status_async,
|
||||
self._august_gateway.access_token,
|
||||
device_id,
|
||||
hyper_bridge,
|
||||
)
|
||||
|
||||
async def async_lock_async(self, device_id, hyper_bridge):
|
||||
"""Lock the device but do not wait for a response since it will come via pubnub."""
|
||||
return await self._async_call_api_op_requires_bridge(
|
||||
device_id,
|
||||
self._api.async_lock_async,
|
||||
self._august_gateway.access_token,
|
||||
device_id,
|
||||
hyper_bridge,
|
||||
)
|
||||
|
||||
async def async_unlock(self, device_id):
|
||||
@ -263,13 +285,14 @@ class AugustData(AugustSubscriberMixin):
|
||||
device_id,
|
||||
)
|
||||
|
||||
async def async_unlock_async(self, device_id):
|
||||
async def async_unlock_async(self, device_id, hyper_bridge):
|
||||
"""Unlock the device but do not wait for a response since it will come via pubnub."""
|
||||
return await self._async_call_api_op_requires_bridge(
|
||||
device_id,
|
||||
self._api.async_unlock_async,
|
||||
self._august_gateway.access_token,
|
||||
device_id,
|
||||
hyper_bridge,
|
||||
)
|
||||
|
||||
async def _async_call_api_op_requires_bridge(
|
||||
|
@ -39,17 +39,22 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
|
||||
self._attr_unique_id = f"{self._device_id:s}_lock"
|
||||
self._update_from_data()
|
||||
|
||||
@property
|
||||
def _hyper_bridge(self):
|
||||
"""Check if the lock has a paired hyper bridge."""
|
||||
return bool(self._detail.bridge and self._detail.bridge.hyper_bridge)
|
||||
|
||||
async def async_lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
if self._data.activity_stream.pubnub.connected:
|
||||
await self._data.async_lock_async(self._device_id)
|
||||
await self._data.async_lock_async(self._device_id, self._hyper_bridge)
|
||||
return
|
||||
await self._call_lock_operation(self._data.async_lock)
|
||||
|
||||
async def async_unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
if self._data.activity_stream.pubnub.connected:
|
||||
await self._data.async_unlock_async(self._device_id)
|
||||
await self._data.async_unlock_async(self._device_id, self._hyper_bridge)
|
||||
return
|
||||
await self._call_lock_operation(self._data.async_unlock)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "august",
|
||||
"name": "August",
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"requirements": ["yalexs==1.1.17"],
|
||||
"requirements": ["yalexs==1.1.19"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"dhcp": [
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"requirements": ["bimmer_connected==0.8.7"],
|
||||
"requirements": ["bimmer_connected==0.8.10"],
|
||||
"codeowners": ["@gerard33", "@rikroe"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
|
@ -474,8 +474,8 @@ def _is_ignored_device(discovery_info: ssdp.SsdpServiceInfo) -> bool:
|
||||
# Special cases for devices with other discovery methods (e.g. mDNS), or
|
||||
# that advertise multiple unrelated (sent in separate discovery packets)
|
||||
# UPnP devices.
|
||||
manufacturer = discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER, "").lower()
|
||||
model = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "").lower()
|
||||
manufacturer = (discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER) or "").lower()
|
||||
model = (discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "").lower()
|
||||
|
||||
if manufacturer.startswith("xbmc") or model == "kodi":
|
||||
# kodi
|
||||
|
@ -71,7 +71,14 @@ from .const import (
|
||||
TRANSITION_STROBE,
|
||||
)
|
||||
from .entity import FluxOnOffEntity
|
||||
from .util import _effect_brightness, _flux_color_mode_to_hass, _hass_color_modes
|
||||
from .util import (
|
||||
_effect_brightness,
|
||||
_flux_color_mode_to_hass,
|
||||
_hass_color_modes,
|
||||
_min_rgb_brightness,
|
||||
_min_rgbw_brightness,
|
||||
_min_rgbwc_brightness,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -313,13 +320,13 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
|
||||
"""Determine brightness from kwargs or current value."""
|
||||
if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None:
|
||||
brightness = self.brightness
|
||||
if not brightness:
|
||||
# If the brightness was previously 0, the light
|
||||
# will not turn on unless brightness is at least 1
|
||||
# If the device was on and brightness was not
|
||||
# set, it means it was masked by an effect
|
||||
brightness = 255 if self.is_on else 1
|
||||
return brightness
|
||||
# If the brightness was previously 0, the light
|
||||
# will not turn on unless brightness is at least 1
|
||||
#
|
||||
# We previously had a problem with the brightness
|
||||
# sometimes reporting as 0 when an effect was in progress,
|
||||
# however this has since been resolved in the upstream library
|
||||
return max(1, brightness)
|
||||
|
||||
async def _async_set_mode(self, **kwargs: Any) -> None:
|
||||
"""Set an effect or color mode."""
|
||||
@ -348,6 +355,8 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
|
||||
return
|
||||
# Handle switch to RGB Color Mode
|
||||
if rgb := kwargs.get(ATTR_RGB_COLOR):
|
||||
if not self._device.requires_turn_on:
|
||||
rgb = _min_rgb_brightness(rgb)
|
||||
red, green, blue = rgb
|
||||
await self._device.async_set_levels(red, green, blue, brightness=brightness)
|
||||
return
|
||||
@ -355,13 +364,18 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
|
||||
if rgbw := kwargs.get(ATTR_RGBW_COLOR):
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
rgbw = rgbw_brightness(rgbw, brightness)
|
||||
if not self._device.requires_turn_on:
|
||||
rgbw = _min_rgbw_brightness(rgbw)
|
||||
await self._device.async_set_levels(*rgbw)
|
||||
return
|
||||
# Handle switch to RGBWW Color Mode
|
||||
if rgbcw := kwargs.get(ATTR_RGBWW_COLOR):
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness)
|
||||
await self._device.async_set_levels(*rgbcw_to_rgbwc(rgbcw))
|
||||
rgbwc = rgbcw_to_rgbwc(rgbcw)
|
||||
if not self._device.requires_turn_on:
|
||||
rgbwc = _min_rgbwc_brightness(rgbwc)
|
||||
await self._device.async_set_levels(*rgbwc)
|
||||
return
|
||||
if (white := kwargs.get(ATTR_WHITE)) is not None:
|
||||
await self._device.async_set_levels(w=white)
|
||||
|
@ -3,42 +3,41 @@
|
||||
"name": "Flux LED/MagicHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||
"requirements": ["flux_led==0.27.45"],
|
||||
"requirements": ["flux_led==0.28.4"],
|
||||
"quality_scale": "platinum",
|
||||
"codeowners": ["@icemanch"],
|
||||
"iot_class": "local_push",
|
||||
"dhcp": [
|
||||
{
|
||||
"macaddress": "18B905*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "249494*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "7CB94C*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "B4E842*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "F0FE6B*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "8CCE4E*",
|
||||
"hostname": "lwip*"
|
||||
},
|
||||
{
|
||||
"hostname": "zengge_[0-9a-f][0-9a-f]_*"
|
||||
},
|
||||
{
|
||||
"macaddress": "C82E47*",
|
||||
"hostname": "sta*"
|
||||
}
|
||||
{
|
||||
"macaddress": "18B905*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "249494*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "7CB94C*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "B4E842*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "F0FE6B*",
|
||||
"hostname": "[ba][lk]*"
|
||||
},
|
||||
{
|
||||
"macaddress": "8CCE4E*",
|
||||
"hostname": "lwip*"
|
||||
},
|
||||
{
|
||||
"hostname": "zengge_[0-9a-f][0-9a-f]_*"
|
||||
},
|
||||
{
|
||||
"macaddress": "C82E47*",
|
||||
"hostname": "sta*"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -34,3 +34,26 @@ def _flux_color_mode_to_hass(
|
||||
def _effect_brightness(brightness: int) -> int:
|
||||
"""Convert hass brightness to effect brightness."""
|
||||
return round(brightness / 255 * 100)
|
||||
|
||||
|
||||
def _min_rgb_brightness(rgb: tuple[int, int, int]) -> tuple[int, int, int]:
|
||||
"""Ensure the RGB value will not turn off the device from a turn on command."""
|
||||
if all(byte == 0 for byte in rgb):
|
||||
return (1, 1, 1)
|
||||
return rgb
|
||||
|
||||
|
||||
def _min_rgbw_brightness(rgbw: tuple[int, int, int, int]) -> tuple[int, int, int, int]:
|
||||
"""Ensure the RGBW value will not turn off the device from a turn on command."""
|
||||
if all(byte == 0 for byte in rgbw):
|
||||
return (1, 1, 1, 0)
|
||||
return rgbw
|
||||
|
||||
|
||||
def _min_rgbwc_brightness(
|
||||
rgbwc: tuple[int, int, int, int, int]
|
||||
) -> tuple[int, int, int, int, int]:
|
||||
"""Ensure the RGBWC value will not turn off the device from a turn on command."""
|
||||
if all(byte == 0 for byte in rgbwc):
|
||||
return (1, 1, 1, 0, 0)
|
||||
return rgbwc
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
import ipaddress
|
||||
import logging
|
||||
import os
|
||||
@ -352,8 +353,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
|
||||
@callback
|
||||
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
|
||||
options = dict(entry.options)
|
||||
data = dict(entry.data)
|
||||
options = deepcopy(dict(entry.options))
|
||||
data = deepcopy(dict(entry.data))
|
||||
modified = False
|
||||
for importable_option in CONFIG_OPTIONS:
|
||||
if importable_option not in entry.options and importable_option in entry.data:
|
||||
|
@ -2,9 +2,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
from typing import Final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -116,7 +118,7 @@ DEFAULT_DOMAINS = [
|
||||
"water_heater",
|
||||
]
|
||||
|
||||
_EMPTY_ENTITY_FILTER = {
|
||||
_EMPTY_ENTITY_FILTER: Final = {
|
||||
CONF_INCLUDE_DOMAINS: [],
|
||||
CONF_EXCLUDE_DOMAINS: [],
|
||||
CONF_INCLUDE_ENTITIES: [],
|
||||
@ -151,7 +153,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Choose specific domains in bridge mode."""
|
||||
if user_input is not None:
|
||||
entity_filter = _EMPTY_ENTITY_FILTER.copy()
|
||||
entity_filter = deepcopy(_EMPTY_ENTITY_FILTER)
|
||||
entity_filter[CONF_INCLUDE_DOMAINS] = user_input[CONF_INCLUDE_DOMAINS]
|
||||
self.hk_data[CONF_FILTER] = entity_filter
|
||||
return await self.async_step_pairing()
|
||||
@ -492,7 +494,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
self.hk_options.update(user_input)
|
||||
return await self.async_step_include_exclude()
|
||||
|
||||
self.hk_options = dict(self.config_entry.options)
|
||||
self.hk_options = deepcopy(dict(self.config_entry.options))
|
||||
entity_filter = self.hk_options.get(CONF_FILTER, {})
|
||||
homekit_mode = self.hk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
|
||||
domains = entity_filter.get(CONF_INCLUDE_DOMAINS, [])
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "HomeKit",
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit",
|
||||
"requirements": [
|
||||
"HAP-python==4.3.0",
|
||||
"HAP-python==4.4.0",
|
||||
"fnvhash==0.1.0",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
|
@ -174,6 +174,7 @@ class Thermostat(HomeAccessory):
|
||||
self.char_target_heat_cool.override_properties(
|
||||
valid_values=self.hc_hass_to_homekit
|
||||
)
|
||||
self.char_target_heat_cool.allow_invalid_client_values = True
|
||||
# Current and target temperature characteristics
|
||||
|
||||
self.char_current_temp = serv_thermostat.configure_char(
|
||||
@ -252,7 +253,6 @@ class Thermostat(HomeAccessory):
|
||||
|
||||
hvac_mode = state.state
|
||||
homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode]
|
||||
|
||||
# Homekit will reset the mode when VIEWING the temp
|
||||
# Ignore it if its the same mode
|
||||
if (
|
||||
@ -282,7 +282,7 @@ class Thermostat(HomeAccessory):
|
||||
target_hc,
|
||||
hc_fallback,
|
||||
)
|
||||
target_hc = hc_fallback
|
||||
self.char_target_heat_cool.value = target_hc = hc_fallback
|
||||
break
|
||||
|
||||
params[ATTR_HVAC_MODE] = self.hc_homekit_to_hass[target_hc]
|
||||
|
@ -206,7 +206,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return bool(hass.config_entries.async_entries(DOMAIN))
|
||||
|
||||
conf = dict(conf)
|
||||
|
||||
hass.data[DATA_KNX_CONFIG] = conf
|
||||
|
||||
# Only import if we haven't before.
|
||||
@ -223,19 +222,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Load a config entry."""
|
||||
conf = hass.data.get(DATA_KNX_CONFIG)
|
||||
|
||||
# When reloading
|
||||
# `conf` is None when reloading the integration or no `knx` key in configuration.yaml
|
||||
if conf is None:
|
||||
conf = await async_integration_yaml_config(hass, DOMAIN)
|
||||
if not conf or DOMAIN not in conf:
|
||||
return False
|
||||
|
||||
conf = conf[DOMAIN]
|
||||
|
||||
# If user didn't have configuration.yaml config, generate defaults
|
||||
if conf is None:
|
||||
conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN]
|
||||
|
||||
_conf = await async_integration_yaml_config(hass, DOMAIN)
|
||||
if not _conf or DOMAIN not in _conf:
|
||||
_LOGGER.warning(
|
||||
"No `knx:` key found in configuration.yaml. See "
|
||||
"https://www.home-assistant.io/integrations/knx/ "
|
||||
"for KNX entity configuration documentation"
|
||||
)
|
||||
# generate defaults
|
||||
conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
|
||||
else:
|
||||
conf = _conf[DOMAIN]
|
||||
config = {**conf, **entry.data}
|
||||
|
||||
try:
|
||||
@ -363,7 +362,6 @@ class KNXModule:
|
||||
self.entry.async_on_unload(
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
)
|
||||
|
||||
self.entry.async_on_unload(self.entry.add_update_listener(async_update_entry))
|
||||
|
||||
def init_xknx(self) -> None:
|
||||
@ -403,7 +401,6 @@ class KNXModule:
|
||||
route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False),
|
||||
auto_reconnect=True,
|
||||
)
|
||||
|
||||
return ConnectionConfig(auto_reconnect=True)
|
||||
|
||||
async def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
|
||||
|
@ -44,7 +44,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_tunnels: list
|
||||
_tunnels: list[GatewayDescriptor]
|
||||
_gateway_ip: str = ""
|
||||
_gateway_port: int = DEFAULT_MCAST_PORT
|
||||
|
||||
@ -64,25 +64,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_type(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle connection type configuration."""
|
||||
errors: dict = {}
|
||||
supported_connection_types = CONF_KNX_INITIAL_CONNECTION_TYPES.copy()
|
||||
fields = {}
|
||||
|
||||
if user_input is None:
|
||||
gateways = await scan_for_gateways()
|
||||
|
||||
if gateways:
|
||||
supported_connection_types.insert(0, CONF_KNX_AUTOMATIC)
|
||||
self._tunnels = [
|
||||
gateway for gateway in gateways if gateway.supports_tunnelling
|
||||
]
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(
|
||||
supported_connection_types
|
||||
)
|
||||
}
|
||||
|
||||
if user_input is not None:
|
||||
connection_type = user_input[CONF_KNX_CONNECTION_TYPE]
|
||||
if connection_type == CONF_KNX_AUTOMATIC:
|
||||
@ -99,6 +80,22 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_step_manual_tunnel()
|
||||
|
||||
errors: dict = {}
|
||||
supported_connection_types = CONF_KNX_INITIAL_CONNECTION_TYPES.copy()
|
||||
fields = {}
|
||||
gateways = await scan_for_gateways()
|
||||
|
||||
if gateways:
|
||||
# add automatic only if a gateway responded
|
||||
supported_connection_types.insert(0, CONF_KNX_AUTOMATIC)
|
||||
self._tunnels = [
|
||||
gateway for gateway in gateways if gateway.supports_tunnelling
|
||||
]
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(supported_connection_types)
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="type", data_schema=vol.Schema(fields), errors=errors
|
||||
)
|
||||
@ -107,8 +104,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict | None = None
|
||||
) -> FlowResult:
|
||||
"""General setup."""
|
||||
errors: dict = {}
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=f"{CONF_KNX_TUNNELING.capitalize()} @ {user_input[CONF_HOST]}",
|
||||
@ -129,6 +124,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
errors: dict = {}
|
||||
fields = {
|
||||
vol.Required(CONF_HOST, default=self._gateway_ip): str,
|
||||
vol.Required(CONF_PORT, default=self._gateway_port): vol.Coerce(int),
|
||||
@ -149,8 +145,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful or if only one gateway was found."""
|
||||
errors: dict = {}
|
||||
|
||||
if user_input is not None:
|
||||
gateway: GatewayDescriptor = next(
|
||||
gateway
|
||||
@ -163,6 +157,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_step_manual_tunnel()
|
||||
|
||||
errors: dict = {}
|
||||
tunnel_repr = {
|
||||
str(tunnel) for tunnel in self._tunnels if tunnel.supports_tunnelling
|
||||
}
|
||||
@ -182,8 +177,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_routing(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Routing setup."""
|
||||
errors: dict = {}
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=CONF_KNX_ROUTING.capitalize(),
|
||||
@ -205,6 +198,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
errors: dict = {}
|
||||
fields = {
|
||||
vol.Required(
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
|
||||
@ -434,7 +428,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
)
|
||||
|
||||
|
||||
async def scan_for_gateways(stop_on_found: int = 0) -> list:
|
||||
async def scan_for_gateways(stop_on_found: int = 0) -> list[GatewayDescriptor]:
|
||||
"""Scan for gateways within the network."""
|
||||
xknx = XKNX()
|
||||
gatewayscanner = GatewayScanner(
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "nexia",
|
||||
"name": "Nexia/American Standard/Trane",
|
||||
"requirements": ["nexia==0.9.12"],
|
||||
"requirements": ["nexia==0.9.13"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||
"config_flow": true,
|
||||
|
@ -281,10 +281,10 @@ class BlockSleepingClimate(
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set preset mode."""
|
||||
if not self._attr_preset_modes:
|
||||
if not self._preset_modes:
|
||||
return
|
||||
|
||||
preset_index = self._attr_preset_modes.index(preset_mode)
|
||||
preset_index = self._preset_modes.index(preset_mode)
|
||||
|
||||
if preset_index == 0:
|
||||
await self.set_state_full_path(schedule=0)
|
||||
|
@ -316,6 +316,10 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
)
|
||||
ssid_filter = {ssid: ssid for ssid in sorted(ssids)}
|
||||
|
||||
selected_ssids_to_filter = [
|
||||
ssid for ssid in self.controller.option_ssid_filter if ssid in ssid_filter
|
||||
]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="device_tracker",
|
||||
data_schema=vol.Schema(
|
||||
@ -333,7 +337,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
default=self.controller.option_track_devices,
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_SSID_FILTER, default=self.controller.option_ssid_filter
|
||||
CONF_SSID_FILTER, default=selected_ssids_to_filter
|
||||
): cv.multi_select(ssid_filter),
|
||||
vol.Optional(
|
||||
CONF_DETECTION_TIME,
|
||||
@ -365,12 +369,18 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
client.mac
|
||||
] = f"{client.name or client.hostname} ({client.mac})"
|
||||
|
||||
selected_clients_to_block = [
|
||||
client
|
||||
for client in self.options.get(CONF_BLOCK_CLIENT, [])
|
||||
if client in clients_to_block
|
||||
]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="client_control",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_BLOCK_CLIENT, default=self.options[CONF_BLOCK_CLIENT]
|
||||
CONF_BLOCK_CLIENT, default=selected_clients_to_block
|
||||
): cv.multi_select(clients_to_block),
|
||||
vol.Optional(
|
||||
CONF_POE_CLIENTS,
|
||||
|
@ -88,7 +88,12 @@ from .discovery import (
|
||||
async_discover_node_values,
|
||||
async_discover_single_value,
|
||||
)
|
||||
from .helpers import async_enable_statistics, get_device_id, get_unique_id
|
||||
from .helpers import (
|
||||
async_enable_statistics,
|
||||
get_device_id,
|
||||
get_device_id_ext,
|
||||
get_unique_id,
|
||||
)
|
||||
from .migrate import async_migrate_discovered_value
|
||||
from .services import ZWaveServices
|
||||
|
||||
@ -116,17 +121,27 @@ def register_node_in_dev_reg(
|
||||
) -> device_registry.DeviceEntry:
|
||||
"""Register node in dev reg."""
|
||||
device_id = get_device_id(client, node)
|
||||
# If a device already exists but it doesn't match the new node, it means the node
|
||||
# was replaced with a different device and the device needs to be removeed so the
|
||||
# new device can be created. Otherwise if the device exists and the node is the same,
|
||||
# the node was replaced with the same device model and we can reuse the device.
|
||||
if (device := dev_reg.async_get_device({device_id})) and (
|
||||
device.model != node.device_config.label
|
||||
or device.manufacturer != node.device_config.manufacturer
|
||||
device_id_ext = get_device_id_ext(client, node)
|
||||
device = dev_reg.async_get_device({device_id})
|
||||
|
||||
# Replace the device if it can be determined that this node is not the
|
||||
# same product as it was previously.
|
||||
if (
|
||||
device_id_ext
|
||||
and device
|
||||
and len(device.identifiers) == 2
|
||||
and device_id_ext not in device.identifiers
|
||||
):
|
||||
remove_device_func(device)
|
||||
device = None
|
||||
|
||||
if device_id_ext:
|
||||
ids = {device_id, device_id_ext}
|
||||
else:
|
||||
ids = {device_id}
|
||||
|
||||
params = {
|
||||
ATTR_IDENTIFIERS: {device_id},
|
||||
ATTR_IDENTIFIERS: ids,
|
||||
ATTR_SW_VERSION: node.firmware_version,
|
||||
ATTR_NAME: node.name
|
||||
or node.device_config.description
|
||||
@ -338,7 +353,14 @@ async def async_setup_entry( # noqa: C901
|
||||
device = dev_reg.async_get_device({dev_id})
|
||||
# We assert because we know the device exists
|
||||
assert device
|
||||
if not replaced:
|
||||
if replaced:
|
||||
discovered_value_ids.pop(device.id, None)
|
||||
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
f"{DOMAIN}_{client.driver.controller.home_id}.{node.node_id}.node_status_remove_entity",
|
||||
)
|
||||
else:
|
||||
remove_device(device)
|
||||
|
||||
@callback
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.const import (
|
||||
CONF_DOMAIN,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_TYPE,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@ -172,7 +173,17 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
|
||||
|
||||
meter_endpoints: dict[int, dict[str, Any]] = defaultdict(dict)
|
||||
|
||||
for entry in entity_registry.async_entries_for_device(registry, device_id):
|
||||
for entry in entity_registry.async_entries_for_device(
|
||||
registry, device_id, include_disabled_entities=False
|
||||
):
|
||||
# If an entry is unavailable, it is possible that the underlying value
|
||||
# is no longer valid. Additionally, if an entry is disabled, its
|
||||
# underlying value is not being monitored by HA so we shouldn't allow
|
||||
# actions against it.
|
||||
if (
|
||||
state := hass.states.get(entry.entity_id)
|
||||
) and state.state == STATE_UNAVAILABLE:
|
||||
continue
|
||||
entity_action = {**base_action, CONF_ENTITY_ID: entry.entity_id}
|
||||
actions.append({**entity_action, CONF_TYPE: SERVICE_REFRESH_VALUE})
|
||||
if entry.domain == LOCK_DOMAIN:
|
||||
@ -187,10 +198,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]:
|
||||
value_id = entry.unique_id.split(".")[1]
|
||||
# If this unique ID doesn't have a value ID, we know it is the node status
|
||||
# sensor which doesn't have any relevant actions
|
||||
if re.match(VALUE_ID_REGEX, value_id):
|
||||
value = node.values[value_id]
|
||||
else:
|
||||
if not re.match(VALUE_ID_REGEX, value_id):
|
||||
continue
|
||||
value = node.values[value_id]
|
||||
# If the value has the meterType CC specific value, we can add a reset_meter
|
||||
# action for it
|
||||
if CC_SPECIFIC_METER_TYPE in value.metadata.cc_specific:
|
||||
|
@ -20,6 +20,7 @@ from .migrate import async_add_migration_entity_value
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_VALUE_UPDATED = "value updated"
|
||||
EVENT_VALUE_REMOVED = "value removed"
|
||||
EVENT_DEAD = "dead"
|
||||
EVENT_ALIVE = "alive"
|
||||
|
||||
@ -99,6 +100,10 @@ class ZWaveBaseEntity(Entity):
|
||||
self.async_on_remove(
|
||||
self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.info.node.on(EVENT_VALUE_REMOVED, self._value_removed)
|
||||
)
|
||||
|
||||
for status_event in (EVENT_ALIVE, EVENT_DEAD):
|
||||
self.async_on_remove(
|
||||
self.info.node.on(status_event, self._node_status_alive_or_dead)
|
||||
@ -171,7 +176,7 @@ class ZWaveBaseEntity(Entity):
|
||||
|
||||
@callback
|
||||
def _value_changed(self, event_data: dict) -> None:
|
||||
"""Call when (one of) our watched values changes.
|
||||
"""Call when a value associated with our node changes.
|
||||
|
||||
Should not be overridden by subclasses.
|
||||
"""
|
||||
@ -193,6 +198,25 @@ class ZWaveBaseEntity(Entity):
|
||||
self.on_value_update()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _value_removed(self, event_data: dict) -> None:
|
||||
"""Call when a value associated with our node is removed.
|
||||
|
||||
Should not be overridden by subclasses.
|
||||
"""
|
||||
value_id = event_data["value"].value_id
|
||||
|
||||
if value_id != self.info.primary_value.value_id:
|
||||
return
|
||||
|
||||
LOGGER.debug(
|
||||
"[%s] Primary value %s is being removed",
|
||||
self.entity_id,
|
||||
value_id,
|
||||
)
|
||||
|
||||
self.hass.async_create_task(self.async_remove())
|
||||
|
||||
@callback
|
||||
def get_zwave_value(
|
||||
self,
|
||||
|
@ -66,6 +66,19 @@ def get_device_id(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str]:
|
||||
return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")
|
||||
|
||||
|
||||
@callback
|
||||
def get_device_id_ext(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str] | None:
|
||||
"""Get extended device registry identifier for Z-Wave node."""
|
||||
if None in (node.manufacturer_id, node.product_type, node.product_id):
|
||||
return None
|
||||
|
||||
domain, dev_id = get_device_id(client, node)
|
||||
return (
|
||||
domain,
|
||||
f"{dev_id}-{node.manufacturer_id}:{node.product_type}:{node.product_id}",
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def get_home_and_node_id_from_device_id(device_id: tuple[str, ...]) -> list[str]:
|
||||
"""
|
||||
|
@ -520,4 +520,11 @@ class ZWaveNodeStatusSensor(SensorEntity):
|
||||
self.async_poll_value,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self.unique_id}_remove_entity",
|
||||
self.async_remove,
|
||||
)
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 12
|
||||
PATCH_VERSION: Final = "9"
|
||||
PATCH_VERSION: Final = "10"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||
|
@ -584,7 +584,9 @@ class DeviceRegistry:
|
||||
configuration_url=device["configuration_url"],
|
||||
# type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625
|
||||
connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc]
|
||||
disabled_by=device["disabled_by"],
|
||||
disabled_by=DeviceEntryDisabler(device["disabled_by"])
|
||||
if device["disabled_by"]
|
||||
else None,
|
||||
entry_type=DeviceEntryType(device["entry_type"])
|
||||
if device["entry_type"]
|
||||
else None,
|
||||
|
@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2
|
||||
# Adafruit_BBIO==1.1.1
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.3.0
|
||||
HAP-python==4.4.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==1.5.1
|
||||
@ -388,7 +388,7 @@ beautifulsoup4==4.10.0
|
||||
bellows==0.29.0
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.8.7
|
||||
bimmer_connected==0.8.10
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
@ -659,7 +659,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.27.45
|
||||
flux_led==0.28.4
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@ -1066,7 +1066,7 @@ nettigo-air-monitor==1.2.1
|
||||
neurio==0.3.1
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.9.12
|
||||
nexia==0.9.13
|
||||
|
||||
# homeassistant.components.nextcloud
|
||||
nextcloudmonitor==1.1.0
|
||||
@ -2466,7 +2466,7 @@ xs1-api-client==3.0.0
|
||||
yalesmartalarmclient==0.3.4
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.1.17
|
||||
yalexs==1.1.19
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.8
|
||||
|
@ -7,7 +7,7 @@
|
||||
AEMET-OpenData==0.2.1
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.3.0
|
||||
HAP-python==4.4.0
|
||||
|
||||
# homeassistant.components.flick_electric
|
||||
PyFlick==0.0.2
|
||||
@ -257,7 +257,7 @@ base36==0.1.1
|
||||
bellows==0.29.0
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.8.7
|
||||
bimmer_connected==0.8.10
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==1.3.3
|
||||
@ -399,7 +399,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.27.45
|
||||
flux_led==0.28.4
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@ -654,7 +654,7 @@ netmap==0.7.0.2
|
||||
nettigo-air-monitor==1.2.1
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==0.9.12
|
||||
nexia==0.9.13
|
||||
|
||||
# homeassistant.components.nfandroidtv
|
||||
notifications-android-tv==0.1.3
|
||||
@ -1464,7 +1464,7 @@ xmltodict==0.12.0
|
||||
yalesmartalarmclient==0.3.4
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.1.17
|
||||
yalexs==1.1.19
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.8
|
||||
|
@ -162,10 +162,17 @@ async def _create_august_with_devices( # noqa: C901
|
||||
"unlock_return_activities"
|
||||
] = unlock_return_activities_side_effect
|
||||
|
||||
return await _mock_setup_august_with_api_side_effects(
|
||||
api_instance, entry = await _mock_setup_august_with_api_side_effects(
|
||||
hass, api_call_side_effects, pubnub
|
||||
)
|
||||
|
||||
if device_data["locks"]:
|
||||
# Ensure we sync status when the integration is loaded if there
|
||||
# are any locks
|
||||
assert api_instance.async_status_async.mock_calls
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, pubnub):
|
||||
api_instance = MagicMock(name="Api")
|
||||
@ -207,9 +214,10 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects,
|
||||
|
||||
api_instance.async_unlock_async = AsyncMock()
|
||||
api_instance.async_lock_async = AsyncMock()
|
||||
api_instance.async_status_async = AsyncMock()
|
||||
api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"})
|
||||
|
||||
return await _mock_setup_august(hass, api_instance, pubnub)
|
||||
return api_instance, await _mock_setup_august(hass, api_instance, pubnub)
|
||||
|
||||
|
||||
def _mock_august_authentication(token_text, token_timestamp, state):
|
||||
|
@ -39,6 +39,9 @@ from homeassistant.components.light import (
|
||||
ATTR_RGBWW_COLOR,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
ATTR_WHITE,
|
||||
COLOR_MODE_RGB,
|
||||
COLOR_MODE_RGBW,
|
||||
COLOR_MODE_RGBWW,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@ -247,9 +250,11 @@ async def test_rgb_light(hass: HomeAssistant) -> None:
|
||||
blocking=True,
|
||||
)
|
||||
# If the bulb is on and we are using existing brightness
|
||||
# and brightness was 0 it means we could not read it because
|
||||
# an effect is in progress so we use 255
|
||||
bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255)
|
||||
# and brightness was 0 older devices will not be able to turn on
|
||||
# so we need to make sure its at least 1 and that we
|
||||
# call it before the turn on command since the device
|
||||
# does not support auto on
|
||||
bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1)
|
||||
bulb.async_set_levels.reset_mock()
|
||||
|
||||
bulb.brightness = 128
|
||||
@ -304,9 +309,9 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None:
|
||||
assert state.state == STATE_ON
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 128
|
||||
assert attributes[ATTR_COLOR_MODE] == "rgb"
|
||||
assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_RGB
|
||||
assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"]
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_RGB]
|
||||
assert attributes[ATTR_HS_COLOR] == (0, 100)
|
||||
|
||||
await hass.services.async_call(
|
||||
@ -331,6 +336,19 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None:
|
||||
bulb.async_set_levels.reset_mock()
|
||||
bulb.async_turn_on.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (0, 0, 0)},
|
||||
blocking=True,
|
||||
)
|
||||
# If the bulb is off and we are using existing brightness
|
||||
# it has to be at least 1 or the bulb won't turn on
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_levels.assert_called_with(1, 1, 1, brightness=1)
|
||||
bulb.async_set_levels.reset_mock()
|
||||
bulb.async_turn_on.reset_mock()
|
||||
|
||||
# Should still be called with no kwargs
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
@ -357,10 +375,11 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None:
|
||||
blocking=True,
|
||||
)
|
||||
# If the bulb is on and we are using existing brightness
|
||||
# and brightness was 0 it means we could not read it because
|
||||
# an effect is in progress so we use 255
|
||||
# and brightness was 0 we need to set it to at least 1
|
||||
# or the device may not turn on
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255)
|
||||
bulb.async_set_brightness.assert_not_called()
|
||||
bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1)
|
||||
bulb.async_set_levels.reset_mock()
|
||||
|
||||
bulb.brightness = 128
|
||||
@ -395,6 +414,236 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None:
|
||||
bulb.async_set_effect.reset_mock()
|
||||
|
||||
|
||||
async def test_rgbw_light_auto_on(hass: HomeAssistant) -> None:
|
||||
"""Test an rgbw light that does not need the turn on command sent."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
|
||||
unique_id=MAC_ADDRESS,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.requires_turn_on = False
|
||||
bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model
|
||||
bulb.color_modes = {FLUX_COLOR_MODE_RGBW}
|
||||
bulb.color_mode = FLUX_COLOR_MODE_RGBW
|
||||
with _patch_discovery(), _patch_wifibulb(device=bulb):
|
||||
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.bulb_rgbcw_ddeeff"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 128
|
||||
assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_RGBW
|
||||
assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_RGBW]
|
||||
assert attributes[ATTR_HS_COLOR] == (0.0, 83.529)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.async_turn_off.assert_called_once()
|
||||
|
||||
await async_mock_device_turn_off(hass, bulb)
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
bulb.brightness = 0
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (10, 10, 30, 0)},
|
||||
blocking=True,
|
||||
)
|
||||
# If the bulb is off and we are using existing brightness
|
||||
# it has to be at least 1 or the bulb won't turn on
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_levels.assert_called_with(10, 10, 30, 0)
|
||||
bulb.async_set_levels.reset_mock()
|
||||
bulb.async_turn_on.reset_mock()
|
||||
|
||||
# Should still be called with no kwargs
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.async_turn_on.assert_called_once()
|
||||
await async_mock_device_turn_on(hass, bulb)
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
bulb.async_turn_on.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_brightness.assert_called_with(100)
|
||||
bulb.async_set_brightness.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (0, 0, 0, 0)},
|
||||
blocking=True,
|
||||
)
|
||||
# If the bulb is on and we are using existing brightness
|
||||
# and brightness was 0 we need to set it to at least 1
|
||||
# or the device may not turn on
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_brightness.assert_not_called()
|
||||
bulb.async_set_levels.assert_called_with(1, 1, 1, 0)
|
||||
bulb.async_set_levels.reset_mock()
|
||||
|
||||
bulb.brightness = 128
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_levels.assert_called_with(110, 19, 0, 255)
|
||||
bulb.async_set_levels.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_effect.assert_called_once()
|
||||
bulb.async_set_effect.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_effect.assert_called_with("purple_fade", 50, 50)
|
||||
bulb.async_set_effect.reset_mock()
|
||||
|
||||
|
||||
async def test_rgbww_light_auto_on(hass: HomeAssistant) -> None:
|
||||
"""Test an rgbww light that does not need the turn on command sent."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
|
||||
unique_id=MAC_ADDRESS,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_bulb()
|
||||
bulb.requires_turn_on = False
|
||||
bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model
|
||||
bulb.color_modes = {FLUX_COLOR_MODE_RGBWW}
|
||||
bulb.color_mode = FLUX_COLOR_MODE_RGBWW
|
||||
with _patch_discovery(), _patch_wifibulb(device=bulb):
|
||||
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = "light.bulb_rgbcw_ddeeff"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 128
|
||||
assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_RGBWW
|
||||
assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_RGBWW]
|
||||
assert attributes[ATTR_HS_COLOR] == (3.237, 94.51)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.async_turn_off.assert_called_once()
|
||||
|
||||
await async_mock_device_turn_off(hass, bulb)
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
bulb.brightness = 0
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (10, 10, 30, 0, 0)},
|
||||
blocking=True,
|
||||
)
|
||||
# If the bulb is off and we are using existing brightness
|
||||
# it has to be at least 1 or the bulb won't turn on
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_levels.assert_called_with(10, 10, 30, 0, 0)
|
||||
bulb.async_set_levels.reset_mock()
|
||||
bulb.async_turn_on.reset_mock()
|
||||
|
||||
# Should still be called with no kwargs
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
bulb.async_turn_on.assert_called_once()
|
||||
await async_mock_device_turn_on(hass, bulb)
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
bulb.async_turn_on.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_brightness.assert_called_with(100)
|
||||
bulb.async_set_brightness.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (0, 0, 0, 0, 0)},
|
||||
blocking=True,
|
||||
)
|
||||
# If the bulb is on and we are using existing brightness
|
||||
# and brightness was 0 we need to set it to at least 1
|
||||
# or the device may not turn on
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_brightness.assert_not_called()
|
||||
bulb.async_set_levels.assert_called_with(1, 1, 1, 0, 0)
|
||||
bulb.async_set_levels.reset_mock()
|
||||
|
||||
bulb.brightness = 128
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_levels.assert_called_with(14, 0, 30, 255, 255)
|
||||
bulb.async_set_levels.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_effect.assert_called_once()
|
||||
bulb.async_set_effect.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.async_turn_on.assert_not_called()
|
||||
bulb.async_set_effect.assert_called_with("purple_fade", 50, 50)
|
||||
bulb.async_set_effect.reset_mock()
|
||||
|
||||
|
||||
async def test_rgb_cct_light(hass: HomeAssistant) -> None:
|
||||
"""Test an rgb cct light."""
|
||||
config_entry = MockConfigEntry(
|
||||
|
@ -2,9 +2,14 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME
|
||||
from homeassistant.components.homekit.const import (
|
||||
CONF_FILTER,
|
||||
DOMAIN,
|
||||
SHORT_BRIDGE_NAME,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_NAME, CONF_PORT
|
||||
from homeassistant.helpers.entityfilter import CONF_INCLUDE_DOMAINS
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .util import PATH_HOMEKIT, async_init_entry
|
||||
@ -347,6 +352,10 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip):
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "include_exclude"
|
||||
|
||||
# Inject garbage to ensure the options data
|
||||
# is being deep copied and we cannot mutate it in flight
|
||||
config_entry.options[CONF_FILTER][CONF_INCLUDE_DOMAINS].append("garbage")
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"entities": ["climate.old"], "include_exclude_mode": "exclude"},
|
||||
|
@ -1266,6 +1266,7 @@ async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver):
|
||||
await hass.async_block_till_done()
|
||||
hap = acc.char_target_heat_cool.to_HAP()
|
||||
assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_HEAT]
|
||||
assert acc.char_target_heat_cool.allow_invalid_client_values is True
|
||||
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT
|
||||
|
||||
acc.char_target_heat_cool.set_value(HC_HEAT_COOL_HEAT)
|
||||
@ -1303,6 +1304,29 @@ async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver):
|
||||
assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT
|
||||
|
||||
acc.char_target_heat_cool.client_update_value(HC_HEAT_COOL_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_OFF
|
||||
hass.states.async_set(
|
||||
entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hk_driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_target_heat_cool_iid,
|
||||
HAP_REPR_VALUE: HC_HEAT_COOL_AUTO,
|
||||
},
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT
|
||||
|
||||
|
||||
async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver):
|
||||
"""Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to cool."""
|
||||
|
@ -473,6 +473,24 @@ def fortrezz_ssa1_siren_state_fixture():
|
||||
return json.loads(load_fixture("zwave_js/fortrezz_ssa1_siren_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="fortrezz_ssa3_siren_state", scope="session")
|
||||
def fortrezz_ssa3_siren_state_fixture():
|
||||
"""Load the fortrezz ssa3 siren node state fixture data."""
|
||||
return json.loads(load_fixture("zwave_js/fortrezz_ssa3_siren_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="zp3111_not_ready_state", scope="session")
|
||||
def zp3111_not_ready_state_fixture():
|
||||
"""Load the zp3111 4-in-1 sensor not-ready node state fixture data."""
|
||||
return json.loads(load_fixture("zwave_js/zp3111-5_not_ready_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="zp3111_state", scope="session")
|
||||
def zp3111_state_fixture():
|
||||
"""Load the zp3111 4-in-1 sensor node state fixture data."""
|
||||
return json.loads(load_fixture("zwave_js/zp3111-5_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def mock_client_fixture(controller_state, version_state, log_config_state):
|
||||
"""Mock a client."""
|
||||
@ -905,3 +923,19 @@ def fortrezz_ssa1_siren_fixture(client, fortrezz_ssa1_siren_state):
|
||||
def firmware_file_fixture():
|
||||
"""Return mock firmware file stream."""
|
||||
return io.BytesIO(bytes(10))
|
||||
|
||||
|
||||
@pytest.fixture(name="zp3111_not_ready")
|
||||
def zp3111_not_ready_fixture(client, zp3111_not_ready_state):
|
||||
"""Mock a zp3111 4-in-1 sensor node in a not-ready state."""
|
||||
node = Node(client, copy.deepcopy(zp3111_not_ready_state))
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="zp3111")
|
||||
def zp3111_fixture(client, zp3111_state):
|
||||
"""Mock a zp3111 4-in-1 sensor node."""
|
||||
node = Node(client, copy.deepcopy(zp3111_state))
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
@ -0,0 +1,68 @@
|
||||
{
|
||||
"nodeId": 22,
|
||||
"index": 0,
|
||||
"status": 1,
|
||||
"ready": false,
|
||||
"isListening": false,
|
||||
"isRouting": true,
|
||||
"isSecure": "unknown",
|
||||
"interviewAttempts": 1,
|
||||
"endpoints": [
|
||||
{
|
||||
"nodeId": 22,
|
||||
"index": 0,
|
||||
"deviceClass": {
|
||||
"basic": {
|
||||
"key": 4,
|
||||
"label": "Routing Slave"
|
||||
},
|
||||
"generic": {
|
||||
"key": 7,
|
||||
"label": "Notification Sensor"
|
||||
},
|
||||
"specific": {
|
||||
"key": 1,
|
||||
"label": "Notification Sensor"
|
||||
},
|
||||
"mandatorySupportedCCs": [],
|
||||
"mandatoryControlledCCs": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"values": [],
|
||||
"isFrequentListening": false,
|
||||
"maxDataRate": 100000,
|
||||
"supportedDataRates": [
|
||||
40000,
|
||||
100000
|
||||
],
|
||||
"protocolVersion": 3,
|
||||
"supportsBeaming": true,
|
||||
"supportsSecurity": false,
|
||||
"nodeType": 1,
|
||||
"deviceClass": {
|
||||
"basic": {
|
||||
"key": 4,
|
||||
"label": "Routing Slave"
|
||||
},
|
||||
"generic": {
|
||||
"key": 7,
|
||||
"label": "Notification Sensor"
|
||||
},
|
||||
"specific": {
|
||||
"key": 1,
|
||||
"label": "Notification Sensor"
|
||||
},
|
||||
"mandatorySupportedCCs": [],
|
||||
"mandatoryControlledCCs": []
|
||||
},
|
||||
"commandClasses": [],
|
||||
"interviewStage": "ProtocolInfo",
|
||||
"statistics": {
|
||||
"commandsTX": 0,
|
||||
"commandsRX": 0,
|
||||
"commandsDroppedRX": 0,
|
||||
"commandsDroppedTX": 0,
|
||||
"timeoutResponse": 0
|
||||
}
|
||||
}
|
706
tests/components/zwave_js/fixtures/zp3111-5_state.json
Normal file
706
tests/components/zwave_js/fixtures/zp3111-5_state.json
Normal file
@ -0,0 +1,706 @@
|
||||
{
|
||||
"nodeId": 22,
|
||||
"index": 0,
|
||||
"installerIcon": 3079,
|
||||
"userIcon": 3079,
|
||||
"status": 2,
|
||||
"ready": true,
|
||||
"isListening": false,
|
||||
"isRouting": true,
|
||||
"isSecure": false,
|
||||
"manufacturerId": 265,
|
||||
"productId": 8449,
|
||||
"productType": 8225,
|
||||
"firmwareVersion": "5.1",
|
||||
"zwavePlusVersion": 1,
|
||||
"deviceConfig": {
|
||||
"filename": "/cache/db/devices/0x0109/zp3111-5.json",
|
||||
"isEmbedded": true,
|
||||
"manufacturer": "Vision Security",
|
||||
"manufacturerId": 265,
|
||||
"label": "ZP3111-5",
|
||||
"description": "4-in-1 Sensor",
|
||||
"devices": [
|
||||
{
|
||||
"productType": 8225,
|
||||
"productId": 8449
|
||||
}
|
||||
],
|
||||
"firmwareVersion": {
|
||||
"min": "0.0",
|
||||
"max": "255.255"
|
||||
},
|
||||
"paramInformation": {
|
||||
"_map": {}
|
||||
},
|
||||
"metadata": {
|
||||
"inclusion": "To add the ZP3111 to the Z-Wave network (inclusion), place the Z-Wave primary controller into inclusion mode. Press the Program Switch of ZP3111 for sending the NIF. After sending NIF, Z-Wave will send the auto inclusion, otherwise, ZP3111 will go to sleep after 20 seconds.",
|
||||
"exclusion": "To remove the ZP3111 from the Z-Wave network (exclusion), place the Z-Wave primary controller into “exclusion” mode, and following its instruction to delete the ZP3111 to the controller. Press the Program Switch of ZP3111 once to be excluded.",
|
||||
"reset": "Remove cover to trigged tamper switch, LED flash once & send out Alarm Report. Press Program Switch 10 times within 10 seconds, ZP3111 will send the “Device Reset Locally Notification” command and reset to the factory default. (Remark: This is to be used only in the case of primary controller being inoperable or otherwise unavailable.)",
|
||||
"manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf"
|
||||
}
|
||||
},
|
||||
"label": "ZP3111-5",
|
||||
"interviewAttempts": 1,
|
||||
"endpoints": [
|
||||
{
|
||||
"nodeId": 22,
|
||||
"index": 0,
|
||||
"installerIcon": 3079,
|
||||
"userIcon": 3079,
|
||||
"deviceClass": {
|
||||
"basic": {
|
||||
"key": 4,
|
||||
"label": "Routing Slave"
|
||||
},
|
||||
"generic": {
|
||||
"key": 7,
|
||||
"label": "Notification Sensor"
|
||||
},
|
||||
"specific": {
|
||||
"key": 1,
|
||||
"label": "Notification Sensor"
|
||||
},
|
||||
"mandatorySupportedCCs": [],
|
||||
"mandatoryControlledCCs": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"values": [
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "libraryType",
|
||||
"propertyName": "libraryType",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Library type",
|
||||
"states": {
|
||||
"0": "Unknown",
|
||||
"1": "Static Controller",
|
||||
"2": "Controller",
|
||||
"3": "Enhanced Slave",
|
||||
"4": "Slave",
|
||||
"5": "Installer",
|
||||
"6": "Routing Slave",
|
||||
"7": "Bridge Controller",
|
||||
"8": "Device under Test",
|
||||
"9": "N/A",
|
||||
"10": "AV Remote",
|
||||
"11": "AV Device"
|
||||
}
|
||||
},
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "protocolVersion",
|
||||
"propertyName": "protocolVersion",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "string",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Z-Wave protocol version"
|
||||
},
|
||||
"value": "4.5"
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "firmwareVersions",
|
||||
"propertyName": "firmwareVersions",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "string[]",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Z-Wave chip firmware versions"
|
||||
},
|
||||
"value": [
|
||||
"5.1",
|
||||
"10.1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "hardwareVersion",
|
||||
"propertyName": "hardwareVersion",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Z-Wave chip hardware version"
|
||||
},
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 114,
|
||||
"commandClassName": "Manufacturer Specific",
|
||||
"property": "manufacturerId",
|
||||
"propertyName": "manufacturerId",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Manufacturer ID",
|
||||
"min": 0,
|
||||
"max": 65535
|
||||
},
|
||||
"value": 265
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 114,
|
||||
"commandClassName": "Manufacturer Specific",
|
||||
"property": "productType",
|
||||
"propertyName": "productType",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Product type",
|
||||
"min": 0,
|
||||
"max": 65535
|
||||
},
|
||||
"value": 8225
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 114,
|
||||
"commandClassName": "Manufacturer Specific",
|
||||
"property": "productId",
|
||||
"propertyName": "productId",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Product ID",
|
||||
"min": 0,
|
||||
"max": 65535
|
||||
},
|
||||
"value": 8449
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 128,
|
||||
"commandClassName": "Battery",
|
||||
"property": "level",
|
||||
"propertyName": "level",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Battery level",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"unit": "%"
|
||||
},
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 128,
|
||||
"commandClassName": "Battery",
|
||||
"property": "isLow",
|
||||
"propertyName": "isLow",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "boolean",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Low battery level"
|
||||
},
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 113,
|
||||
"commandClassName": "Notification",
|
||||
"property": "Home Security",
|
||||
"propertyKey": "Cover status",
|
||||
"propertyName": "Home Security",
|
||||
"propertyKeyName": "Cover status",
|
||||
"ccVersion": 4,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Cover status",
|
||||
"ccSpecific": {
|
||||
"notificationType": 7
|
||||
},
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "idle",
|
||||
"3": "Tampering, product cover removed"
|
||||
}
|
||||
},
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 113,
|
||||
"commandClassName": "Notification",
|
||||
"property": "Home Security",
|
||||
"propertyKey": "Motion sensor status",
|
||||
"propertyName": "Home Security",
|
||||
"propertyKeyName": "Motion sensor status",
|
||||
"ccVersion": 4,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Motion sensor status",
|
||||
"ccSpecific": {
|
||||
"notificationType": 7
|
||||
},
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "idle",
|
||||
"8": "Motion detection"
|
||||
}
|
||||
},
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 113,
|
||||
"commandClassName": "Notification",
|
||||
"property": "alarmType",
|
||||
"propertyName": "alarmType",
|
||||
"ccVersion": 4,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Alarm Type",
|
||||
"min": 0,
|
||||
"max": 255
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 113,
|
||||
"commandClassName": "Notification",
|
||||
"property": "alarmLevel",
|
||||
"propertyName": "alarmLevel",
|
||||
"ccVersion": 4,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Alarm Level",
|
||||
"min": 0,
|
||||
"max": 255
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 49,
|
||||
"commandClassName": "Multilevel Sensor",
|
||||
"property": "Air temperature",
|
||||
"propertyName": "Air temperature",
|
||||
"ccVersion": 7,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Air temperature",
|
||||
"ccSpecific": {
|
||||
"sensorType": 1,
|
||||
"scale": 0
|
||||
},
|
||||
"unit": "°C"
|
||||
},
|
||||
"value": 21.98
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 49,
|
||||
"commandClassName": "Multilevel Sensor",
|
||||
"property": "Illuminance",
|
||||
"propertyName": "Illuminance",
|
||||
"ccVersion": 7,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Illuminance",
|
||||
"ccSpecific": {
|
||||
"sensorType": 3,
|
||||
"scale": 0
|
||||
},
|
||||
"unit": "%"
|
||||
},
|
||||
"value": 7.31
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 49,
|
||||
"commandClassName": "Multilevel Sensor",
|
||||
"property": "Humidity",
|
||||
"propertyName": "Humidity",
|
||||
"ccVersion": 7,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Humidity",
|
||||
"ccSpecific": {
|
||||
"sensorType": 5,
|
||||
"scale": 0
|
||||
},
|
||||
"unit": "%"
|
||||
},
|
||||
"value": 51.98
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 112,
|
||||
"commandClassName": "Configuration",
|
||||
"property": 1,
|
||||
"propertyName": "Temperature Scale",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"label": "Temperature Scale",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"states": {
|
||||
"0": "Celsius",
|
||||
"1": "Fahrenheit"
|
||||
},
|
||||
"valueSize": 1,
|
||||
"format": 0,
|
||||
"allowManualEntry": false,
|
||||
"isFromConfig": true
|
||||
},
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 112,
|
||||
"commandClassName": "Configuration",
|
||||
"property": 2,
|
||||
"propertyName": "Temperature offset",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"label": "Temperature offset",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 50,
|
||||
"valueSize": 1,
|
||||
"format": 0,
|
||||
"allowManualEntry": true,
|
||||
"isFromConfig": true
|
||||
},
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 112,
|
||||
"commandClassName": "Configuration",
|
||||
"property": 3,
|
||||
"propertyName": "Humidity",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"description": "Configure Relative Humidity",
|
||||
"label": "Humidity",
|
||||
"default": 10,
|
||||
"min": 1,
|
||||
"max": 50,
|
||||
"unit": "percent",
|
||||
"valueSize": 1,
|
||||
"format": 0,
|
||||
"allowManualEntry": true,
|
||||
"isFromConfig": true
|
||||
},
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 112,
|
||||
"commandClassName": "Configuration",
|
||||
"property": 4,
|
||||
"propertyName": "Light Sensor",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"label": "Light Sensor",
|
||||
"default": 10,
|
||||
"min": 1,
|
||||
"max": 50,
|
||||
"unit": "percent",
|
||||
"valueSize": 1,
|
||||
"format": 0,
|
||||
"allowManualEntry": true,
|
||||
"isFromConfig": true
|
||||
},
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 112,
|
||||
"commandClassName": "Configuration",
|
||||
"property": 5,
|
||||
"propertyName": "Trigger Interval",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"description": "Set the trigger interval for motion sensor re-activation.",
|
||||
"label": "Trigger Interval",
|
||||
"default": 180,
|
||||
"min": 1,
|
||||
"max": 255,
|
||||
"unit": "seconds",
|
||||
"valueSize": 1,
|
||||
"format": 1,
|
||||
"allowManualEntry": true,
|
||||
"isFromConfig": true
|
||||
},
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 112,
|
||||
"commandClassName": "Configuration",
|
||||
"property": 6,
|
||||
"propertyName": "Motion Sensor Sensitivity",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"description": "Adjust sensitivity of the motion sensor.",
|
||||
"label": "Motion Sensor Sensitivity",
|
||||
"default": 4,
|
||||
"min": 1,
|
||||
"max": 7,
|
||||
"states": {
|
||||
"1": "highest",
|
||||
"2": "higher",
|
||||
"3": "high",
|
||||
"4": "normal",
|
||||
"5": "low",
|
||||
"6": "lower",
|
||||
"7": "lowest"
|
||||
},
|
||||
"valueSize": 1,
|
||||
"format": 0,
|
||||
"allowManualEntry": false,
|
||||
"isFromConfig": true
|
||||
},
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 112,
|
||||
"commandClassName": "Configuration",
|
||||
"property": 7,
|
||||
"propertyName": "LED indicator mode",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"label": "LED indicator mode",
|
||||
"default": 3,
|
||||
"min": 1,
|
||||
"max": 3,
|
||||
"states": {
|
||||
"1": "Off",
|
||||
"2": "Pulsing Temperature, Flashing Motion",
|
||||
"3": "Flashing Temperature and Motion"
|
||||
},
|
||||
"valueSize": 1,
|
||||
"format": 0,
|
||||
"allowManualEntry": false,
|
||||
"isFromConfig": true
|
||||
},
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 132,
|
||||
"commandClassName": "Wake Up",
|
||||
"property": "wakeUpInterval",
|
||||
"propertyName": "wakeUpInterval",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"default": 3600,
|
||||
"readable": false,
|
||||
"writeable": true,
|
||||
"label": "Wake Up interval",
|
||||
"min": 600,
|
||||
"max": 604800,
|
||||
"steps": 600
|
||||
},
|
||||
"value": 3600
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 132,
|
||||
"commandClassName": "Wake Up",
|
||||
"property": "controllerNodeId",
|
||||
"propertyName": "controllerNodeId",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Node ID of the controller"
|
||||
},
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"isFrequentListening": false,
|
||||
"maxDataRate": 100000,
|
||||
"supportedDataRates": [
|
||||
40000,
|
||||
100000
|
||||
],
|
||||
"protocolVersion": 3,
|
||||
"supportsBeaming": true,
|
||||
"supportsSecurity": false,
|
||||
"nodeType": 1,
|
||||
"zwavePlusNodeType": 0,
|
||||
"zwavePlusRoleType": 6,
|
||||
"deviceClass": {
|
||||
"basic": {
|
||||
"key": 4,
|
||||
"label": "Routing Slave"
|
||||
},
|
||||
"generic": {
|
||||
"key": 7,
|
||||
"label": "Notification Sensor"
|
||||
},
|
||||
"specific": {
|
||||
"key": 1,
|
||||
"label": "Notification Sensor"
|
||||
},
|
||||
"mandatorySupportedCCs": [],
|
||||
"mandatoryControlledCCs": []
|
||||
},
|
||||
"commandClasses": [
|
||||
{
|
||||
"id": 94,
|
||||
"name": "Z-Wave Plus Info",
|
||||
"version": 2,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 134,
|
||||
"name": "Version",
|
||||
"version": 2,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 114,
|
||||
"name": "Manufacturer Specific",
|
||||
"version": 2,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 90,
|
||||
"name": "Device Reset Locally",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 133,
|
||||
"name": "Association",
|
||||
"version": 2,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 89,
|
||||
"name": "Association Group Information",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 115,
|
||||
"name": "Powerlevel",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 128,
|
||||
"name": "Battery",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 113,
|
||||
"name": "Notification",
|
||||
"version": 4,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"name": "Multilevel Sensor",
|
||||
"version": 7,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 112,
|
||||
"name": "Configuration",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 132,
|
||||
"name": "Wake Up",
|
||||
"version": 2,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 122,
|
||||
"name": "Firmware Update Meta Data",
|
||||
"version": 2,
|
||||
"isSecure": false
|
||||
}
|
||||
],
|
||||
"interviewStage": "Complete",
|
||||
"deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0109:0x2021:0x2101:5.1",
|
||||
"statistics": {
|
||||
"commandsTX": 39,
|
||||
"commandsRX": 38,
|
||||
"commandsDroppedRX": 0,
|
||||
"commandsDroppedTX": 0,
|
||||
"timeoutResponse": 0
|
||||
},
|
||||
"highestSecurityClass": -1
|
||||
}
|
@ -11,6 +11,7 @@ from homeassistant.components import automation
|
||||
from homeassistant.components.zwave_js import DOMAIN, device_action
|
||||
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry
|
||||
@ -583,3 +584,23 @@ async def test_failure_scenarios(
|
||||
)
|
||||
== {}
|
||||
)
|
||||
|
||||
|
||||
async def test_unavailable_entity_actions(
|
||||
hass: HomeAssistant,
|
||||
client: Client,
|
||||
lock_schlage_be469: Node,
|
||||
integration: ConfigEntry,
|
||||
) -> None:
|
||||
"""Test unavailable entities are not included in actions list."""
|
||||
entity_id_unavailable = "binary_sensor.touchscreen_deadbolt_home_security_intrusion"
|
||||
hass.states.async_set(entity_id_unavailable, STATE_UNAVAILABLE, force_update=True)
|
||||
await hass.async_block_till_done()
|
||||
node = lock_schlage_be469
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
device = dev_reg.async_get_device({get_device_id(client, node)})
|
||||
assert device
|
||||
actions = await async_get_device_automations(hass, "action", device.id)
|
||||
assert not any(
|
||||
action.get("entity_id") == entity_id_unavailable for action in actions
|
||||
)
|
||||
|
@ -12,7 +12,11 @@ from homeassistant.components.zwave_js.const import DOMAIN
|
||||
from homeassistant.components.zwave_js.helpers import get_device_id
|
||||
from homeassistant.config_entries import DISABLED_USER, ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
|
||||
from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY
|
||||
|
||||
@ -159,7 +163,7 @@ async def test_new_entity_on_value_added(hass, multisensor_6, client, integratio
|
||||
|
||||
|
||||
async def test_on_node_added_ready(hass, multisensor_6_state, client, integration):
|
||||
"""Test we handle a ready node added event."""
|
||||
"""Test we handle a node added event with a ready node."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
node = Node(client, deepcopy(multisensor_6_state))
|
||||
event = {"node": node}
|
||||
@ -182,38 +186,34 @@ async def test_on_node_added_ready(hass, multisensor_6_state, client, integratio
|
||||
assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
|
||||
|
||||
|
||||
async def test_on_node_added_not_ready(hass, multisensor_6_state, client, integration):
|
||||
"""Test we handle a non ready node added event."""
|
||||
async def test_on_node_added_not_ready(
|
||||
hass, zp3111_not_ready_state, client, integration
|
||||
):
|
||||
"""Test we handle a node added event with a non-ready node."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests.
|
||||
node = Node(client, node_data)
|
||||
node.data["ready"] = False
|
||||
event = {"node": node}
|
||||
air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}"
|
||||
device_id = f"{client.driver.controller.home_id}-{zp3111_not_ready_state['nodeId']}"
|
||||
|
||||
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
assert len(hass.states.async_all()) == 0
|
||||
assert not dev_reg.devices
|
||||
|
||||
assert not state # entity and device not yet added
|
||||
assert not dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, air_temperature_device_id)}
|
||||
event = Event(
|
||||
type="node added",
|
||||
data={
|
||||
"source": "controller",
|
||||
"event": "node added",
|
||||
"node": deepcopy(zp3111_not_ready_state),
|
||||
},
|
||||
)
|
||||
|
||||
client.driver.controller.emit("node added", event)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
# the only entity is the node status sensor
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
assert not state # entity not yet added but device added in registry
|
||||
assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
|
||||
|
||||
node.data["ready"] = True
|
||||
node.emit("ready", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
|
||||
assert state # entity added
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
# no extended device identifier yet
|
||||
assert len(device.identifiers) == 1
|
||||
|
||||
|
||||
async def test_existing_node_ready(hass, client, multisensor_6, integration):
|
||||
@ -221,12 +221,157 @@ async def test_existing_node_ready(hass, client, multisensor_6, integration):
|
||||
dev_reg = dr.async_get(hass)
|
||||
node = multisensor_6
|
||||
air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}"
|
||||
air_temperature_device_id_ext = (
|
||||
f"{air_temperature_device_id}-{node.manufacturer_id}:"
|
||||
f"{node.product_type}:{node.product_id}"
|
||||
)
|
||||
|
||||
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
|
||||
assert state # entity and device added
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
|
||||
assert device
|
||||
assert device == dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, air_temperature_device_id_ext)}
|
||||
)
|
||||
|
||||
|
||||
async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integration):
|
||||
"""Test we handle a non-ready node that exists during integration setup."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
node = zp3111_not_ready
|
||||
device_id = f"{client.driver.controller.home_id}-{node.node_id}"
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device.name == f"Node {node.node_id}"
|
||||
assert not device.manufacturer
|
||||
assert not device.model
|
||||
assert not device.sw_version
|
||||
|
||||
# the only entity is the node status sensor
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
# no extended device identifier yet
|
||||
assert len(device.identifiers) == 1
|
||||
|
||||
|
||||
async def test_existing_node_not_replaced_when_not_ready(
|
||||
hass, zp3111, zp3111_not_ready_state, zp3111_state, client, integration
|
||||
):
|
||||
"""Test when a node added event with a non-ready node is received.
|
||||
|
||||
The existing node should not be replaced, and no customization should be lost.
|
||||
"""
|
||||
dev_reg = dr.async_get(hass)
|
||||
er_reg = er.async_get(hass)
|
||||
kitchen_area = ar.async_get(hass).async_create("Kitchen")
|
||||
|
||||
device_id = f"{client.driver.controller.home_id}-{zp3111.node_id}"
|
||||
device_id_ext = (
|
||||
f"{device_id}-{zp3111.manufacturer_id}:"
|
||||
f"{zp3111.product_type}:{zp3111.product_id}"
|
||||
)
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
assert device.name == "4-in-1 Sensor"
|
||||
assert not device.name_by_user
|
||||
assert device.manufacturer == "Vision Security"
|
||||
assert device.model == "ZP3111-5"
|
||||
assert device.sw_version == "5.1"
|
||||
assert not device.area_id
|
||||
assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)})
|
||||
|
||||
motion_entity = "binary_sensor.4_in_1_sensor_home_security_motion_detection"
|
||||
state = hass.states.get(motion_entity)
|
||||
assert state
|
||||
assert state.name == "4-in-1 Sensor: Home Security - Motion detection"
|
||||
|
||||
dev_reg.async_update_device(
|
||||
device.id, name_by_user="Custom Device Name", area_id=kitchen_area.id
|
||||
)
|
||||
|
||||
custom_device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert custom_device
|
||||
assert custom_device.name == "4-in-1 Sensor"
|
||||
assert custom_device.name_by_user == "Custom Device Name"
|
||||
assert custom_device.manufacturer == "Vision Security"
|
||||
assert custom_device.model == "ZP3111-5"
|
||||
assert device.sw_version == "5.1"
|
||||
assert custom_device.area_id == kitchen_area.id
|
||||
assert custom_device == dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, device_id_ext)}
|
||||
)
|
||||
|
||||
custom_entity = "binary_sensor.custom_motion_sensor"
|
||||
er_reg.async_update_entity(
|
||||
motion_entity, new_entity_id=custom_entity, name="Custom Entity Name"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(custom_entity)
|
||||
assert state
|
||||
assert state.name == "Custom Entity Name"
|
||||
assert not hass.states.get(motion_entity)
|
||||
|
||||
event = Event(
|
||||
type="node added",
|
||||
data={
|
||||
"source": "controller",
|
||||
"event": "node added",
|
||||
"node": deepcopy(zp3111_not_ready_state),
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)})
|
||||
assert device.id == custom_device.id
|
||||
assert device.identifiers == custom_device.identifiers
|
||||
assert device.name == f"Node {zp3111.node_id}"
|
||||
assert device.name_by_user == "Custom Device Name"
|
||||
assert not device.manufacturer
|
||||
assert not device.model
|
||||
assert not device.sw_version
|
||||
assert device.area_id == kitchen_area.id
|
||||
|
||||
state = hass.states.get(custom_entity)
|
||||
assert state
|
||||
assert state.name == "Custom Entity Name"
|
||||
|
||||
event = Event(
|
||||
type="ready",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "ready",
|
||||
"nodeId": zp3111_state["nodeId"],
|
||||
"nodeState": deepcopy(zp3111_state),
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)})
|
||||
assert device.id == custom_device.id
|
||||
assert device.identifiers == custom_device.identifiers
|
||||
assert device.name == "4-in-1 Sensor"
|
||||
assert device.name_by_user == "Custom Device Name"
|
||||
assert device.manufacturer == "Vision Security"
|
||||
assert device.model == "ZP3111-5"
|
||||
assert device.area_id == kitchen_area.id
|
||||
assert device.sw_version == "5.1"
|
||||
|
||||
state = hass.states.get(custom_entity)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.name == "Custom Entity Name"
|
||||
|
||||
|
||||
async def test_null_name(hass, client, null_name_check, integration):
|
||||
@ -235,38 +380,6 @@ async def test_null_name(hass, client, null_name_check, integration):
|
||||
assert hass.states.get(f"switch.node_{node.node_id}")
|
||||
|
||||
|
||||
async def test_existing_node_not_ready(hass, client, multisensor_6):
|
||||
"""Test we handle a non ready node that exists during integration setup."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
node = multisensor_6
|
||||
node.data = deepcopy(node.data) # Copy to allow modification in tests.
|
||||
node.data["ready"] = False
|
||||
event = {"node": node}
|
||||
air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}"
|
||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
|
||||
assert not state # entity not yet added
|
||||
assert dev_reg.async_get_device( # device should be added
|
||||
identifiers={(DOMAIN, air_temperature_device_id)}
|
||||
)
|
||||
|
||||
node.data["ready"] = True
|
||||
node.emit("ready", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
|
||||
assert state # entity and device added
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
|
||||
|
||||
|
||||
async def test_start_addon(
|
||||
hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon
|
||||
):
|
||||
@ -738,63 +851,460 @@ async def test_node_removed(hass, multisensor_6_state, client, integration):
|
||||
assert not dev_reg.async_get(old_device.id)
|
||||
|
||||
|
||||
async def test_replace_same_node(hass, multisensor_6_state, client, integration):
|
||||
async def test_replace_same_node(
|
||||
hass, multisensor_6, multisensor_6_state, client, integration
|
||||
):
|
||||
"""Test when a node is replaced with itself that the device remains."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
node = Node(client, deepcopy(multisensor_6_state))
|
||||
device_id = f"{client.driver.controller.home_id}-{node.node_id}"
|
||||
event = {"node": node}
|
||||
node_id = multisensor_6.node_id
|
||||
multisensor_6_state = deepcopy(multisensor_6_state)
|
||||
|
||||
client.driver.controller.emit("node added", event)
|
||||
device_id = f"{client.driver.controller.home_id}-{node_id}"
|
||||
multisensor_6_device_id = (
|
||||
f"{device_id}-{multisensor_6.manufacturer_id}:"
|
||||
f"{multisensor_6.product_type}:{multisensor_6.product_id}"
|
||||
)
|
||||
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
assert device == dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, multisensor_6_device_id)}
|
||||
)
|
||||
assert device.manufacturer == "AEON Labs"
|
||||
assert device.model == "ZW100"
|
||||
dev_id = device.id
|
||||
|
||||
assert hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
|
||||
# A replace node event has the extra field "replaced" set to True
|
||||
# to distinguish it from an exclusion
|
||||
event = Event(
|
||||
type="node removed",
|
||||
data={
|
||||
"source": "controller",
|
||||
"event": "node removed",
|
||||
"replaced": True,
|
||||
"node": multisensor_6_state,
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
old_device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert old_device.id
|
||||
|
||||
event = {"node": node, "replaced": True}
|
||||
# Device should still be there after the node was removed
|
||||
device = dev_reg.async_get(dev_id)
|
||||
assert device
|
||||
|
||||
client.driver.controller.emit("node removed", event)
|
||||
# When the node is replaced, a non-ready node added event is emitted
|
||||
event = Event(
|
||||
type="node added",
|
||||
data={
|
||||
"source": "controller",
|
||||
"event": "node added",
|
||||
"node": {
|
||||
"nodeId": node_id,
|
||||
"index": 0,
|
||||
"status": 4,
|
||||
"ready": False,
|
||||
"isSecure": "unknown",
|
||||
"interviewAttempts": 1,
|
||||
"endpoints": [{"nodeId": node_id, "index": 0, "deviceClass": None}],
|
||||
"values": [],
|
||||
"deviceClass": None,
|
||||
"commandClasses": [],
|
||||
"interviewStage": "None",
|
||||
"statistics": {
|
||||
"commandsTX": 0,
|
||||
"commandsRX": 0,
|
||||
"commandsDroppedRX": 0,
|
||||
"commandsDroppedTX": 0,
|
||||
"timeoutResponse": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Device is still not removed
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
# Assert device has remained
|
||||
assert dev_reg.async_get(old_device.id)
|
||||
|
||||
event = {"node": node}
|
||||
device = dev_reg.async_get(dev_id)
|
||||
assert device
|
||||
|
||||
client.driver.controller.emit("node added", event)
|
||||
event = Event(
|
||||
type="ready",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "ready",
|
||||
"nodeId": node_id,
|
||||
"nodeState": multisensor_6_state,
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
# Assert device has remained
|
||||
assert dev_reg.async_get(old_device.id)
|
||||
|
||||
# Device is the same
|
||||
device = dev_reg.async_get(dev_id)
|
||||
assert device
|
||||
assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device == dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, multisensor_6_device_id)}
|
||||
)
|
||||
assert device.manufacturer == "AEON Labs"
|
||||
assert device.model == "ZW100"
|
||||
|
||||
assert hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
|
||||
|
||||
async def test_replace_different_node(
|
||||
hass, multisensor_6_state, hank_binary_switch_state, client, integration
|
||||
hass,
|
||||
multisensor_6,
|
||||
multisensor_6_state,
|
||||
hank_binary_switch_state,
|
||||
client,
|
||||
integration,
|
||||
):
|
||||
"""Test when a node is replaced with a different node."""
|
||||
hank_binary_switch_state = deepcopy(hank_binary_switch_state)
|
||||
multisensor_6_state = deepcopy(multisensor_6_state)
|
||||
hank_binary_switch_state["nodeId"] = multisensor_6_state["nodeId"]
|
||||
dev_reg = dr.async_get(hass)
|
||||
old_node = Node(client, multisensor_6_state)
|
||||
device_id = f"{client.driver.controller.home_id}-{old_node.node_id}"
|
||||
new_node = Node(client, hank_binary_switch_state)
|
||||
event = {"node": old_node}
|
||||
node_id = multisensor_6.node_id
|
||||
hank_binary_switch_state = deepcopy(hank_binary_switch_state)
|
||||
hank_binary_switch_state["nodeId"] = node_id
|
||||
|
||||
device_id = f"{client.driver.controller.home_id}-{node_id}"
|
||||
multisensor_6_device_id = (
|
||||
f"{device_id}-{multisensor_6.manufacturer_id}:"
|
||||
f"{multisensor_6.product_type}:{multisensor_6.product_id}"
|
||||
)
|
||||
hank_device_id = (
|
||||
f"{device_id}-{hank_binary_switch_state['manufacturerId']}:"
|
||||
f"{hank_binary_switch_state['productType']}:"
|
||||
f"{hank_binary_switch_state['productId']}"
|
||||
)
|
||||
|
||||
client.driver.controller.emit("node added", event)
|
||||
await hass.async_block_till_done()
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
assert device == dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, multisensor_6_device_id)}
|
||||
)
|
||||
assert device.manufacturer == "AEON Labs"
|
||||
assert device.model == "ZW100"
|
||||
dev_id = device.id
|
||||
|
||||
event = {"node": old_node, "replaced": True}
|
||||
assert hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
|
||||
client.driver.controller.emit("node removed", event)
|
||||
# A replace node event has the extra field "replaced" set to True
|
||||
# to distinguish it from an exclusion
|
||||
event = Event(
|
||||
type="node removed",
|
||||
data={
|
||||
"source": "controller",
|
||||
"event": "node removed",
|
||||
"replaced": True,
|
||||
"node": multisensor_6_state,
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Device should still be there after the node was removed
|
||||
device = dev_reg.async_get(dev_id)
|
||||
assert device
|
||||
|
||||
event = {"node": new_node}
|
||||
# When the node is replaced, a non-ready node added event is emitted
|
||||
event = Event(
|
||||
type="node added",
|
||||
data={
|
||||
"source": "controller",
|
||||
"event": "node added",
|
||||
"node": {
|
||||
"nodeId": multisensor_6.node_id,
|
||||
"index": 0,
|
||||
"status": 4,
|
||||
"ready": False,
|
||||
"isSecure": "unknown",
|
||||
"interviewAttempts": 1,
|
||||
"endpoints": [
|
||||
{"nodeId": multisensor_6.node_id, "index": 0, "deviceClass": None}
|
||||
],
|
||||
"values": [],
|
||||
"deviceClass": None,
|
||||
"commandClasses": [],
|
||||
"interviewStage": "None",
|
||||
"statistics": {
|
||||
"commandsTX": 0,
|
||||
"commandsRX": 0,
|
||||
"commandsDroppedRX": 0,
|
||||
"commandsDroppedTX": 0,
|
||||
"timeoutResponse": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
client.driver.controller.emit("node added", event)
|
||||
# Device is still not removed
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
device = dev_reg.async_get(device.id)
|
||||
# assert device is new
|
||||
|
||||
device = dev_reg.async_get(dev_id)
|
||||
assert device
|
||||
|
||||
event = Event(
|
||||
type="ready",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "ready",
|
||||
"nodeId": node_id,
|
||||
"nodeState": hank_binary_switch_state,
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Old device and entities were removed, but the ID is re-used
|
||||
device = dev_reg.async_get(dev_id)
|
||||
assert device
|
||||
assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device == dev_reg.async_get_device(identifiers={(DOMAIN, hank_device_id)})
|
||||
assert not dev_reg.async_get_device(identifiers={(DOMAIN, multisensor_6_device_id)})
|
||||
assert device.manufacturer == "HANK Electronics Ltd."
|
||||
assert device.model == "HKZW-SO01"
|
||||
|
||||
assert not hass.states.get(AIR_TEMPERATURE_SENSOR)
|
||||
assert hass.states.get("switch.smart_plug_with_two_usb_ports")
|
||||
|
||||
|
||||
async def test_node_model_change(hass, zp3111, client, integration):
|
||||
"""Test when a node's model is changed due to an updated device config file.
|
||||
|
||||
The device and entities should not be removed.
|
||||
"""
|
||||
dev_reg = dr.async_get(hass)
|
||||
er_reg = er.async_get(hass)
|
||||
|
||||
device_id = f"{client.driver.controller.home_id}-{zp3111.node_id}"
|
||||
device_id_ext = (
|
||||
f"{device_id}-{zp3111.manufacturer_id}:"
|
||||
f"{zp3111.product_type}:{zp3111.product_id}"
|
||||
)
|
||||
|
||||
# Verify device and entities have default names/ids
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)})
|
||||
assert device.manufacturer == "Vision Security"
|
||||
assert device.model == "ZP3111-5"
|
||||
assert device.name == "4-in-1 Sensor"
|
||||
assert not device.name_by_user
|
||||
|
||||
dev_id = device.id
|
||||
|
||||
motion_entity = "binary_sensor.4_in_1_sensor_home_security_motion_detection"
|
||||
state = hass.states.get(motion_entity)
|
||||
assert state
|
||||
assert state.name == "4-in-1 Sensor: Home Security - Motion detection"
|
||||
|
||||
# Customize device and entity names/ids
|
||||
dev_reg.async_update_device(device.id, name_by_user="Custom Device Name")
|
||||
device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)})
|
||||
assert device
|
||||
assert device.id == dev_id
|
||||
assert device == dev_reg.async_get_device(identifiers={(DOMAIN, device_id_ext)})
|
||||
assert device.manufacturer == "Vision Security"
|
||||
assert device.model == "ZP3111-5"
|
||||
assert device.name == "4-in-1 Sensor"
|
||||
assert device.name_by_user == "Custom Device Name"
|
||||
|
||||
custom_entity = "binary_sensor.custom_motion_sensor"
|
||||
er_reg.async_update_entity(
|
||||
motion_entity, new_entity_id=custom_entity, name="Custom Entity Name"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert not hass.states.get(motion_entity)
|
||||
state = hass.states.get(custom_entity)
|
||||
assert state
|
||||
assert state.name == "Custom Entity Name"
|
||||
|
||||
# Unload the integration
|
||||
assert await hass.config_entries.async_unload(integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert integration.state is ConfigEntryState.NOT_LOADED
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
# Simulate changes to the node labels
|
||||
zp3111.device_config.data["description"] = "New Device Name"
|
||||
zp3111.device_config.data["label"] = "New Device Model"
|
||||
zp3111.device_config.data["manufacturer"] = "New Device Manufacturer"
|
||||
|
||||
# Reload integration, it will re-add the nodes
|
||||
integration.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Device name changes, but the customization is the same
|
||||
device = dev_reg.async_get(dev_id)
|
||||
assert device
|
||||
assert device.id == dev_id
|
||||
assert device.manufacturer == "New Device Manufacturer"
|
||||
assert device.model == "New Device Model"
|
||||
assert device.name == "New Device Name"
|
||||
assert device.name_by_user == "Custom Device Name"
|
||||
|
||||
assert not hass.states.get(motion_entity)
|
||||
state = hass.states.get(custom_entity)
|
||||
assert state
|
||||
assert state.name == "Custom Entity Name"
|
||||
|
||||
|
||||
async def test_disabled_node_status_entity_on_node_replaced(
|
||||
hass, zp3111_state, zp3111, client, integration
|
||||
):
|
||||
"""Test that when a node replacement event is received the node status sensor is removed."""
|
||||
node_status_entity = "sensor.4_in_1_sensor_node_status"
|
||||
state = hass.states.get(node_status_entity)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
event = Event(
|
||||
type="node removed",
|
||||
data={
|
||||
"source": "controller",
|
||||
"event": "node removed",
|
||||
"replaced": True,
|
||||
"node": zp3111_state,
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(node_status_entity)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_disabled_entity_on_value_removed(hass, zp3111, client, integration):
|
||||
"""Test that when entity primary values are removed the entity is removed."""
|
||||
er_reg = er.async_get(hass)
|
||||
|
||||
# re-enable this default-disabled entity
|
||||
sensor_cover_entity = "sensor.4_in_1_sensor_home_security_cover_status"
|
||||
er_reg.async_update_entity(entity_id=sensor_cover_entity, disabled_by=None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# must reload the integration when enabling an entity
|
||||
await hass.config_entries.async_unload(integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert integration.state is ConfigEntryState.NOT_LOADED
|
||||
integration.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert integration.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(sensor_cover_entity)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# check for expected entities
|
||||
binary_cover_entity = (
|
||||
"binary_sensor.4_in_1_sensor_home_security_tampering_product_cover_removed"
|
||||
)
|
||||
state = hass.states.get(binary_cover_entity)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
battery_level_entity = "sensor.4_in_1_sensor_battery_level"
|
||||
state = hass.states.get(battery_level_entity)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
unavailable_entities = {
|
||||
state.entity_id
|
||||
for state in hass.states.async_all()
|
||||
if state.state == STATE_UNAVAILABLE
|
||||
}
|
||||
|
||||
# This value ID removal does not remove any entity
|
||||
event = Event(
|
||||
type="value removed",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value removed",
|
||||
"nodeId": zp3111.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Wake Up",
|
||||
"commandClass": 132,
|
||||
"endpoint": 0,
|
||||
"property": "wakeUpInterval",
|
||||
"prevValue": 3600,
|
||||
"propertyName": "wakeUpInterval",
|
||||
},
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert all(state != STATE_UNAVAILABLE for state in hass.states.async_all())
|
||||
|
||||
# This value ID removal only affects the battery level entity
|
||||
event = Event(
|
||||
type="value removed",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value removed",
|
||||
"nodeId": zp3111.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Battery",
|
||||
"commandClass": 128,
|
||||
"endpoint": 0,
|
||||
"property": "level",
|
||||
"prevValue": 100,
|
||||
"propertyName": "level",
|
||||
},
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(battery_level_entity)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# This value ID removal affects its multiple notification sensors
|
||||
event = Event(
|
||||
type="value removed",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value removed",
|
||||
"nodeId": zp3111.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Notification",
|
||||
"commandClass": 113,
|
||||
"endpoint": 0,
|
||||
"property": "Home Security",
|
||||
"propertyKey": "Cover status",
|
||||
"prevValue": 0,
|
||||
"propertyName": "Home Security",
|
||||
"propertyKeyName": "Cover status",
|
||||
},
|
||||
},
|
||||
)
|
||||
client.driver.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(binary_cover_entity)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
state = hass.states.get(sensor_cover_entity)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# existing entities and the entities with removed values should be unavailable
|
||||
new_unavailable_entities = {
|
||||
state.entity_id
|
||||
for state in hass.states.async_all()
|
||||
if state.state == STATE_UNAVAILABLE
|
||||
}
|
||||
assert (
|
||||
unavailable_entities
|
||||
| {battery_level_entity, binary_cover_entity, sensor_cover_entity}
|
||||
== new_unavailable_entities
|
||||
)
|
||||
|
@ -640,6 +640,7 @@ async def test_loading_saving_data(hass, registry, area_registry):
|
||||
identifiers={("hue", "abc")},
|
||||
manufacturer="manufacturer",
|
||||
model="light",
|
||||
entry_type=device_registry.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
assert orig_light4.id == orig_light3.id
|
||||
@ -679,6 +680,15 @@ async def test_loading_saving_data(hass, registry, area_registry):
|
||||
assert orig_light == new_light
|
||||
assert orig_light4 == new_light4
|
||||
|
||||
# Ensure enums converted
|
||||
for (old, new) in (
|
||||
(orig_via, new_via),
|
||||
(orig_light, new_light),
|
||||
(orig_light4, new_light4),
|
||||
):
|
||||
assert old.disabled_by is new.disabled_by
|
||||
assert old.entry_type is new.entry_type
|
||||
|
||||
# Ensure a save/load cycle does not keep suggested area
|
||||
new_kitchen_light = registry2.async_get_device({("hue", "999")})
|
||||
assert orig_kitchen_light.suggested_area == "Kitchen"
|
||||
|
Loading…
x
Reference in New Issue
Block a user