mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 08:07:45 +00:00
Merge pull request #47422 from home-assistant/rc
This commit is contained in:
commit
91ac4554a2
@ -39,6 +39,7 @@ from .const import (
|
||||
DATA_COORDINATOR,
|
||||
DOMAIN,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
LOGGER,
|
||||
)
|
||||
@ -142,12 +143,21 @@ def _standardize_geography_config_entry(hass, config_entry):
|
||||
if not config_entry.options:
|
||||
# If the config entry doesn't already have any options set, set defaults:
|
||||
entry_updates["options"] = {CONF_SHOW_ON_MAP: True}
|
||||
if CONF_INTEGRATION_TYPE not in config_entry.data:
|
||||
# If the config entry data doesn't contain the integration type, add it:
|
||||
entry_updates["data"] = {
|
||||
**config_entry.data,
|
||||
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
}
|
||||
if config_entry.data.get(CONF_INTEGRATION_TYPE) not in [
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
]:
|
||||
# If the config entry data doesn't contain an integration type that we know
|
||||
# about, infer it from the data we have:
|
||||
entry_updates["data"] = {**config_entry.data}
|
||||
if CONF_CITY in config_entry.data:
|
||||
entry_updates["data"][
|
||||
CONF_INTEGRATION_TYPE
|
||||
] = INTEGRATION_TYPE_GEOGRAPHY_NAME
|
||||
else:
|
||||
entry_updates["data"][
|
||||
CONF_INTEGRATION_TYPE
|
||||
] = INTEGRATION_TYPE_GEOGRAPHY_COORDS
|
||||
|
||||
if not entry_updates:
|
||||
return
|
||||
|
@ -22,7 +22,6 @@ from homeassistant.const import (
|
||||
LENGTH_MILES,
|
||||
PRESSURE_HPA,
|
||||
PRESSURE_INHG,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@ -31,7 +30,6 @@ from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.distance import convert as distance_convert
|
||||
from homeassistant.util.pressure import convert as pressure_convert
|
||||
from homeassistant.util.temperature import convert as temp_convert
|
||||
|
||||
from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity
|
||||
from .const import (
|
||||
@ -102,10 +100,6 @@ def _forecast_dict(
|
||||
precipitation = (
|
||||
distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) * 1000
|
||||
)
|
||||
if temp:
|
||||
temp = temp_convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
if temp_low:
|
||||
temp_low = temp_convert(temp_low, TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
if wind_speed:
|
||||
wind_speed = distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS)
|
||||
|
||||
@ -260,6 +254,7 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity):
|
||||
|
||||
if self.forecast_type == DAILY:
|
||||
use_datetime = False
|
||||
forecast_dt = dt_util.start_of_local_day(forecast_dt)
|
||||
precipitation = self._get_cc_value(
|
||||
forecast, CC_ATTR_PRECIPITATION_DAILY
|
||||
)
|
||||
|
@ -77,6 +77,7 @@ _NOT_SPEED_INTERVAL = "interval"
|
||||
_NOT_SPEED_IDLE = "idle"
|
||||
_NOT_SPEED_FAVORITE = "favorite"
|
||||
_NOT_SPEED_SLEEP = "sleep"
|
||||
_NOT_SPEED_SILENT = "silent"
|
||||
|
||||
_NOT_SPEEDS_FILTER = {
|
||||
_NOT_SPEED_OFF,
|
||||
@ -85,6 +86,7 @@ _NOT_SPEEDS_FILTER = {
|
||||
_NOT_SPEED_SMART,
|
||||
_NOT_SPEED_INTERVAL,
|
||||
_NOT_SPEED_IDLE,
|
||||
_NOT_SPEED_SILENT,
|
||||
_NOT_SPEED_SLEEP,
|
||||
_NOT_SPEED_FAVORITE,
|
||||
}
|
||||
@ -652,7 +654,7 @@ def speed_list_without_preset_modes(speed_list: List):
|
||||
output: ["1", "2", "3", "4", "5", "6", "7"]
|
||||
|
||||
input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"]
|
||||
output: ["Silent", "Medium", "High", "Strong"]
|
||||
output: ["Medium", "High", "Strong"]
|
||||
"""
|
||||
|
||||
return [speed for speed in speed_list if speed.lower() not in _NOT_SPEEDS_FILTER]
|
||||
@ -674,7 +676,7 @@ def preset_modes_from_speed_list(speed_list: List):
|
||||
output: ["smart"]
|
||||
|
||||
input: ["Auto", "Silent", "Favorite", "Idle", "Medium", "High", "Strong"]
|
||||
output: ["Auto", "Favorite", "Idle"]
|
||||
output: ["Auto", "Silent", "Favorite", "Idle"]
|
||||
"""
|
||||
|
||||
return [
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20210302.3"
|
||||
"home-assistant-frontend==20210302.4"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
@ -35,6 +35,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN, CoreState, callback
|
||||
from homeassistant.exceptions import ConditionError
|
||||
from homeassistant.helpers import condition
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import (
|
||||
@ -439,12 +440,16 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
|
||||
current_state = STATE_ON
|
||||
else:
|
||||
current_state = HVAC_MODE_OFF
|
||||
long_enough = condition.state(
|
||||
self.hass,
|
||||
self.heater_entity_id,
|
||||
current_state,
|
||||
self.min_cycle_duration,
|
||||
)
|
||||
try:
|
||||
long_enough = condition.state(
|
||||
self.hass,
|
||||
self.heater_entity_id,
|
||||
current_state,
|
||||
self.min_cycle_duration,
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
|
||||
if not long_enough:
|
||||
return
|
||||
|
||||
|
@ -203,25 +203,28 @@ async def async_setup(hass, config):
|
||||
# TCP port when host configured, otherwise serial port
|
||||
port = config[DOMAIN][CONF_PORT]
|
||||
|
||||
# TCP KEEPALIVE will be enabled if value > 0
|
||||
keepalive_idle_timer = config[DOMAIN][CONF_KEEPALIVE_IDLE]
|
||||
if keepalive_idle_timer < 0:
|
||||
_LOGGER.error(
|
||||
"A bogus TCP Keepalive IDLE timer was provided (%d secs), "
|
||||
"default value will be used. "
|
||||
"Recommended values: 60-3600 (seconds)",
|
||||
keepalive_idle_timer,
|
||||
)
|
||||
keepalive_idle_timer = DEFAULT_TCP_KEEPALIVE_IDLE_TIMER
|
||||
elif keepalive_idle_timer == 0:
|
||||
keepalive_idle_timer = None
|
||||
elif keepalive_idle_timer <= 30:
|
||||
_LOGGER.warning(
|
||||
"A very short TCP Keepalive IDLE timer was provided (%d secs), "
|
||||
"and may produce unexpected disconnections from RFlink device."
|
||||
" Recommended values: 60-3600 (seconds)",
|
||||
keepalive_idle_timer,
|
||||
)
|
||||
keepalive_idle_timer = None
|
||||
# TCP KeepAlive only if this is TCP based connection (not serial)
|
||||
if host is not None:
|
||||
# TCP KEEPALIVE will be enabled if value > 0
|
||||
keepalive_idle_timer = config[DOMAIN][CONF_KEEPALIVE_IDLE]
|
||||
if keepalive_idle_timer < 0:
|
||||
_LOGGER.error(
|
||||
"A bogus TCP Keepalive IDLE timer was provided (%d secs), "
|
||||
"it will be disabled. "
|
||||
"Recommended values: 60-3600 (seconds)",
|
||||
keepalive_idle_timer,
|
||||
)
|
||||
keepalive_idle_timer = None
|
||||
elif keepalive_idle_timer == 0:
|
||||
keepalive_idle_timer = None
|
||||
elif keepalive_idle_timer <= 30:
|
||||
_LOGGER.warning(
|
||||
"A very short TCP Keepalive IDLE timer was provided (%d secs) "
|
||||
"and may produce unexpected disconnections from RFlink device."
|
||||
" Recommended values: 60-3600 (seconds)",
|
||||
keepalive_idle_timer,
|
||||
)
|
||||
|
||||
@callback
|
||||
def reconnect(exc=None):
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Support for WeMo device discovery."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import pywemo
|
||||
@ -15,14 +16,9 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util.async_ import gather_with_concurrency
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
# Max number of devices to initialize at once. This limit is in place to
|
||||
# avoid tying up too many executor threads with WeMo device setup.
|
||||
MAX_CONCURRENCY = 3
|
||||
|
||||
# Mapping from Wemo model_name to domain.
|
||||
WEMO_MODEL_DISPATCH = {
|
||||
"Bridge": LIGHT_DOMAIN,
|
||||
@ -118,12 +114,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
static_conf = config.get(CONF_STATIC, [])
|
||||
if static_conf:
|
||||
_LOGGER.debug("Adding statically configured WeMo devices...")
|
||||
for device in await gather_with_concurrency(
|
||||
MAX_CONCURRENCY,
|
||||
for device in await asyncio.gather(
|
||||
*[
|
||||
hass.async_add_executor_job(validate_static_config, host, port)
|
||||
for host, port in static_conf
|
||||
],
|
||||
]
|
||||
):
|
||||
if device:
|
||||
wemo_dispatcher.async_add_unique_device(hass, device)
|
||||
@ -192,44 +187,15 @@ class WemoDiscovery:
|
||||
self._wemo_dispatcher = wemo_dispatcher
|
||||
self._stop = None
|
||||
self._scan_delay = 0
|
||||
self._upnp_entries = set()
|
||||
|
||||
async def async_add_from_upnp_entry(self, entry: pywemo.ssdp.UPNPEntry) -> None:
|
||||
"""Create a WeMoDevice from an UPNPEntry and add it to the dispatcher.
|
||||
|
||||
Uses the self._upnp_entries set to avoid interrogating the same device
|
||||
multiple times.
|
||||
"""
|
||||
if entry in self._upnp_entries:
|
||||
return
|
||||
try:
|
||||
device = await self._hass.async_add_executor_job(
|
||||
pywemo.discovery.device_from_uuid_and_location,
|
||||
entry.udn,
|
||||
entry.location,
|
||||
)
|
||||
except pywemo.PyWeMoException as err:
|
||||
_LOGGER.error("Unable to setup WeMo %r (%s)", entry, err)
|
||||
else:
|
||||
self._wemo_dispatcher.async_add_unique_device(self._hass, device)
|
||||
self._upnp_entries.add(entry)
|
||||
|
||||
async def async_discover_and_schedule(self, *_) -> None:
|
||||
"""Periodically scan the network looking for WeMo devices."""
|
||||
_LOGGER.debug("Scanning network for WeMo devices...")
|
||||
try:
|
||||
# pywemo.ssdp.scan is a light-weight UDP UPnP scan for WeMo devices.
|
||||
entries = await self._hass.async_add_executor_job(pywemo.ssdp.scan)
|
||||
|
||||
# async_add_from_upnp_entry causes multiple HTTP requests to be sent
|
||||
# to the WeMo device for the initial setup of the WeMoDevice
|
||||
# instance. This may take some time to complete. The per-device
|
||||
# setup work is done in parallel to speed up initial setup for the
|
||||
# component.
|
||||
await gather_with_concurrency(
|
||||
MAX_CONCURRENCY,
|
||||
*[self.async_add_from_upnp_entry(entry) for entry in entries],
|
||||
)
|
||||
for device in await self._hass.async_add_executor_job(
|
||||
pywemo.discover_devices
|
||||
):
|
||||
self._wemo_dispatcher.async_add_unique_device(self._hass, device)
|
||||
finally:
|
||||
# Run discovery more frequently after hass has just started.
|
||||
self._scan_delay = min(
|
||||
|
@ -23,7 +23,7 @@ from .gateway import ConnectXiaomiGateway
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"]
|
||||
GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "switch", "light"]
|
||||
SWITCH_PLATFORMS = ["switch"]
|
||||
VACUUM_PLATFORMS = ["vacuum"]
|
||||
|
||||
|
@ -21,9 +21,8 @@ MODELS_SWITCH = [
|
||||
"chuangmi.plug.v2",
|
||||
"chuangmi.plug.hmi205",
|
||||
"chuangmi.plug.hmi206",
|
||||
"lumi.acpartner.v3",
|
||||
]
|
||||
MODELS_VACUUM = ["roborock.vacuum"]
|
||||
MODELS_VACUUM = ["roborock.vacuum", "rockrobo.vacuum"]
|
||||
|
||||
MODELS_ALL_DEVICES = MODELS_SWITCH + MODELS_VACUUM
|
||||
MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY
|
||||
|
@ -22,7 +22,6 @@ from homeassistant.const import (
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
PRESSURE_HPA,
|
||||
TEMP_CELSIUS,
|
||||
@ -38,6 +37,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Xiaomi Miio Sensor"
|
||||
DATA_KEY = "sensor.xiaomi_miio"
|
||||
UNIT_LUMEN = "lm"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@ -302,7 +302,7 @@ class XiaomiGatewayIlluminanceSensor(Entity):
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return LIGHT_LUX
|
||||
return UNIT_LUMEN
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
@ -22,6 +22,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from .const import (
|
||||
CONF_DEVICE,
|
||||
CONF_FLOW_TYPE,
|
||||
CONF_GATEWAY,
|
||||
CONF_MODEL,
|
||||
DOMAIN,
|
||||
SERVICE_SET_POWER_MODE,
|
||||
@ -129,16 +130,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the switch from a config entry."""
|
||||
entities = []
|
||||
|
||||
if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
|
||||
host = config_entry.data[CONF_HOST]
|
||||
token = config_entry.data[CONF_TOKEN]
|
||||
name = config_entry.title
|
||||
model = config_entry.data[CONF_MODEL]
|
||||
unique_id = config_entry.unique_id
|
||||
|
||||
if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE or (
|
||||
config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY
|
||||
and model == "lumi.acpartner.v3"
|
||||
):
|
||||
if DATA_KEY not in hass.data:
|
||||
hass.data[DATA_KEY] = {}
|
||||
|
||||
host = config_entry.data[CONF_HOST]
|
||||
token = config_entry.data[CONF_TOKEN]
|
||||
name = config_entry.title
|
||||
model = config_entry.data[CONF_MODEL]
|
||||
unique_id = config_entry.unique_id
|
||||
|
||||
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
|
||||
|
||||
if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]:
|
||||
|
@ -48,7 +48,8 @@ from .const import (
|
||||
ZWAVE_JS_EVENT,
|
||||
)
|
||||
from .discovery import async_discover_values
|
||||
from .helpers import get_device_id, get_old_value_id, get_unique_id
|
||||
from .helpers import get_device_id
|
||||
from .migrate import async_migrate_discovered_value
|
||||
from .services import ZWaveServices
|
||||
|
||||
CONNECT_TIMEOUT = 10
|
||||
@ -98,31 +99,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
dev_reg = await device_registry.async_get_registry(hass)
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
|
||||
@callback
|
||||
def migrate_entity(platform: str, old_unique_id: str, new_unique_id: str) -> None:
|
||||
"""Check if entity with old unique ID exists, and if so migrate it to new ID."""
|
||||
if entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id):
|
||||
LOGGER.debug(
|
||||
"Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
|
||||
entity_id,
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
try:
|
||||
ent_reg.async_update_entity(
|
||||
entity_id,
|
||||
new_unique_id=new_unique_id,
|
||||
)
|
||||
except ValueError:
|
||||
LOGGER.debug(
|
||||
(
|
||||
"Entity %s can't be migrated because the unique ID is taken. "
|
||||
"Cleaning it up since it is likely no longer valid."
|
||||
),
|
||||
entity_id,
|
||||
)
|
||||
ent_reg.async_remove(entity_id)
|
||||
|
||||
@callback
|
||||
def async_on_node_ready(node: ZwaveNode) -> None:
|
||||
"""Handle node ready event."""
|
||||
@ -136,49 +112,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
LOGGER.debug("Discovered entity: %s", disc_info)
|
||||
|
||||
# This migration logic was added in 2021.3 to handle a breaking change to
|
||||
# the value_id format. Some time in the future, this code block
|
||||
# (as well as get_old_value_id helper and migrate_entity closure) can be
|
||||
# removed.
|
||||
value_ids = [
|
||||
# 2021.2.* format
|
||||
get_old_value_id(disc_info.primary_value),
|
||||
# 2021.3.0b0 format
|
||||
disc_info.primary_value.value_id,
|
||||
]
|
||||
|
||||
new_unique_id = get_unique_id(
|
||||
client.driver.controller.home_id,
|
||||
disc_info.primary_value.value_id,
|
||||
)
|
||||
|
||||
for value_id in value_ids:
|
||||
old_unique_id = get_unique_id(
|
||||
client.driver.controller.home_id,
|
||||
f"{disc_info.primary_value.node.node_id}.{value_id}",
|
||||
)
|
||||
# Most entities have the same ID format, but notification binary sensors
|
||||
# have a state key in their ID so we need to handle them differently
|
||||
if (
|
||||
disc_info.platform == "binary_sensor"
|
||||
and disc_info.platform_hint == "notification"
|
||||
):
|
||||
for state_key in disc_info.primary_value.metadata.states:
|
||||
# ignore idle key (0)
|
||||
if state_key == "0":
|
||||
continue
|
||||
|
||||
migrate_entity(
|
||||
disc_info.platform,
|
||||
f"{old_unique_id}.{state_key}",
|
||||
f"{new_unique_id}.{state_key}",
|
||||
)
|
||||
|
||||
# Once we've iterated through all state keys, we can move on to the
|
||||
# next item
|
||||
continue
|
||||
|
||||
migrate_entity(disc_info.platform, old_unique_id, new_unique_id)
|
||||
|
||||
# the value_id format. Some time in the future, this call (as well as the
|
||||
# helper functions) can be removed.
|
||||
async_migrate_discovered_value(ent_reg, client, disc_info)
|
||||
async_dispatcher_send(
|
||||
hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info
|
||||
)
|
||||
@ -483,11 +419,15 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) ->
|
||||
network_key: str = entry.data[CONF_NETWORK_KEY]
|
||||
|
||||
if not addon_is_installed:
|
||||
addon_manager.async_schedule_install_addon(usb_path, network_key)
|
||||
addon_manager.async_schedule_install_setup_addon(
|
||||
usb_path, network_key, catch_error=True
|
||||
)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
if not addon_is_running:
|
||||
addon_manager.async_schedule_setup_addon(usb_path, network_key)
|
||||
addon_manager.async_schedule_setup_addon(
|
||||
usb_path, network_key, catch_error=True
|
||||
)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
|
||||
@ -497,4 +437,4 @@ def async_ensure_addon_updated(hass: HomeAssistant) -> None:
|
||||
addon_manager: AddonManager = get_addon_manager(hass)
|
||||
if addon_manager.task_in_progress():
|
||||
raise ConfigEntryNotReady
|
||||
addon_manager.async_schedule_update_addon()
|
||||
addon_manager.async_schedule_update_addon(catch_error=True)
|
||||
|
@ -67,8 +67,8 @@ class AddonManager:
|
||||
"""Set up the add-on manager."""
|
||||
self._hass = hass
|
||||
self._install_task: Optional[asyncio.Task] = None
|
||||
self._start_task: Optional[asyncio.Task] = None
|
||||
self._update_task: Optional[asyncio.Task] = None
|
||||
self._setup_task: Optional[asyncio.Task] = None
|
||||
|
||||
def task_in_progress(self) -> bool:
|
||||
"""Return True if any of the add-on tasks are in progress."""
|
||||
@ -76,7 +76,7 @@ class AddonManager:
|
||||
task and not task.done()
|
||||
for task in (
|
||||
self._install_task,
|
||||
self._setup_task,
|
||||
self._start_task,
|
||||
self._update_task,
|
||||
)
|
||||
)
|
||||
@ -125,8 +125,21 @@ class AddonManager:
|
||||
await async_install_addon(self._hass, ADDON_SLUG)
|
||||
|
||||
@callback
|
||||
def async_schedule_install_addon(
|
||||
self, usb_path: str, network_key: str
|
||||
def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||
"""Schedule a task that installs the Z-Wave JS add-on.
|
||||
|
||||
Only schedule a new install task if the there's no running task.
|
||||
"""
|
||||
if not self._install_task or self._install_task.done():
|
||||
LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
|
||||
self._install_task = self._async_schedule_addon_operation(
|
||||
self.async_install_addon, catch_error=catch_error
|
||||
)
|
||||
return self._install_task
|
||||
|
||||
@callback
|
||||
def async_schedule_install_setup_addon(
|
||||
self, usb_path: str, network_key: str, catch_error: bool = False
|
||||
) -> asyncio.Task:
|
||||
"""Schedule a task that installs and sets up the Z-Wave JS add-on.
|
||||
|
||||
@ -136,7 +149,9 @@ class AddonManager:
|
||||
LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
|
||||
self._install_task = self._async_schedule_addon_operation(
|
||||
self.async_install_addon,
|
||||
partial(self.async_setup_addon, usb_path, network_key),
|
||||
partial(self.async_configure_addon, usb_path, network_key),
|
||||
self.async_start_addon,
|
||||
catch_error=catch_error,
|
||||
)
|
||||
return self._install_task
|
||||
|
||||
@ -158,10 +173,11 @@ class AddonManager:
|
||||
if not update_available:
|
||||
return
|
||||
|
||||
await self.async_create_snapshot()
|
||||
await async_update_addon(self._hass, ADDON_SLUG)
|
||||
|
||||
@callback
|
||||
def async_schedule_update_addon(self) -> asyncio.Task:
|
||||
def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||
"""Schedule a task that updates and sets up the Z-Wave JS add-on.
|
||||
|
||||
Only schedule a new update task if the there's no running task.
|
||||
@ -169,7 +185,8 @@ class AddonManager:
|
||||
if not self._update_task or self._update_task.done():
|
||||
LOGGER.info("Trying to update the Z-Wave JS add-on")
|
||||
self._update_task = self._async_schedule_addon_operation(
|
||||
self.async_create_snapshot, self.async_update_addon
|
||||
self.async_update_addon,
|
||||
catch_error=catch_error,
|
||||
)
|
||||
return self._update_task
|
||||
|
||||
@ -178,12 +195,25 @@ class AddonManager:
|
||||
"""Start the Z-Wave JS add-on."""
|
||||
await async_start_addon(self._hass, ADDON_SLUG)
|
||||
|
||||
@callback
|
||||
def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
|
||||
"""Schedule a task that starts the Z-Wave JS add-on.
|
||||
|
||||
Only schedule a new start task if the there's no running task.
|
||||
"""
|
||||
if not self._start_task or self._start_task.done():
|
||||
LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
|
||||
self._start_task = self._async_schedule_addon_operation(
|
||||
self.async_start_addon, catch_error=catch_error
|
||||
)
|
||||
return self._start_task
|
||||
|
||||
@api_error("Failed to stop the Z-Wave JS add-on")
|
||||
async def async_stop_addon(self) -> None:
|
||||
"""Stop the Z-Wave JS add-on."""
|
||||
await async_stop_addon(self._hass, ADDON_SLUG)
|
||||
|
||||
async def async_setup_addon(self, usb_path: str, network_key: str) -> None:
|
||||
async def async_configure_addon(self, usb_path: str, network_key: str) -> None:
|
||||
"""Configure and start Z-Wave JS add-on."""
|
||||
addon_options = await self.async_get_addon_options()
|
||||
|
||||
@ -195,22 +225,22 @@ class AddonManager:
|
||||
if new_addon_options != addon_options:
|
||||
await self.async_set_addon_options(new_addon_options)
|
||||
|
||||
await self.async_start_addon()
|
||||
|
||||
@callback
|
||||
def async_schedule_setup_addon(
|
||||
self, usb_path: str, network_key: str
|
||||
self, usb_path: str, network_key: str, catch_error: bool = False
|
||||
) -> asyncio.Task:
|
||||
"""Schedule a task that configures and starts the Z-Wave JS add-on.
|
||||
|
||||
Only schedule a new setup task if the there's no running task.
|
||||
"""
|
||||
if not self._setup_task or self._setup_task.done():
|
||||
if not self._start_task or self._start_task.done():
|
||||
LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
|
||||
self._setup_task = self._async_schedule_addon_operation(
|
||||
partial(self.async_setup_addon, usb_path, network_key)
|
||||
self._start_task = self._async_schedule_addon_operation(
|
||||
partial(self.async_configure_addon, usb_path, network_key),
|
||||
self.async_start_addon,
|
||||
catch_error=catch_error,
|
||||
)
|
||||
return self._setup_task
|
||||
return self._start_task
|
||||
|
||||
@api_error("Failed to create a snapshot of the Z-Wave JS add-on.")
|
||||
async def async_create_snapshot(self) -> None:
|
||||
@ -227,7 +257,9 @@ class AddonManager:
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_schedule_addon_operation(self, *funcs: Callable) -> asyncio.Task:
|
||||
def _async_schedule_addon_operation(
|
||||
self, *funcs: Callable, catch_error: bool = False
|
||||
) -> asyncio.Task:
|
||||
"""Schedule an add-on task."""
|
||||
|
||||
async def addon_operation() -> None:
|
||||
@ -236,6 +268,8 @@ class AddonManager:
|
||||
try:
|
||||
await func()
|
||||
except AddonError as err:
|
||||
if not catch_error:
|
||||
raise
|
||||
LOGGER.error(err)
|
||||
break
|
||||
|
||||
|
@ -118,25 +118,17 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
super().__init__(config_entry, client, info)
|
||||
self._hvac_modes: Dict[str, Optional[int]] = {}
|
||||
self._hvac_presets: Dict[str, Optional[int]] = {}
|
||||
self._unit_value: ZwaveValue = None
|
||||
self._unit_value: Optional[ZwaveValue] = None
|
||||
|
||||
self._current_mode = self.get_zwave_value(
|
||||
THERMOSTAT_MODE_PROPERTY, command_class=CommandClass.THERMOSTAT_MODE
|
||||
)
|
||||
self._setpoint_values: Dict[ThermostatSetpointType, ZwaveValue] = {}
|
||||
for enum in ThermostatSetpointType:
|
||||
# Some devices don't include a property key so we need to check for value
|
||||
# ID's, both with and without the property key
|
||||
self._setpoint_values[enum] = self.get_zwave_value(
|
||||
THERMOSTAT_SETPOINT_PROPERTY,
|
||||
command_class=CommandClass.THERMOSTAT_SETPOINT,
|
||||
value_property_key=enum.value.key,
|
||||
value_property_key_name=enum.value.name,
|
||||
add_to_watched_value_ids=True,
|
||||
) or self.get_zwave_value(
|
||||
THERMOSTAT_SETPOINT_PROPERTY,
|
||||
command_class=CommandClass.THERMOSTAT_SETPOINT,
|
||||
value_property_key_name=enum.value.name,
|
||||
add_to_watched_value_ids=True,
|
||||
)
|
||||
# Use the first found setpoint value to always determine the temperature unit
|
||||
@ -215,7 +207,11 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
if "f" in self._unit_value.metadata.unit.lower():
|
||||
if (
|
||||
self._unit_value
|
||||
and self._unit_value.metadata.unit
|
||||
and "f" in self._unit_value.metadata.unit.lower()
|
||||
):
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
|
||||
|
@ -117,7 +117,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(
|
||||
version_info.home_id, raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured(user_input)
|
||||
# Make sure we disable any add-on handling
|
||||
# if the controller is reconfigured in a manual step.
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
**user_input,
|
||||
CONF_USE_ADDON: False,
|
||||
CONF_INTEGRATION_CREATED_ADDON: False,
|
||||
}
|
||||
)
|
||||
self.ws_address = user_input[CONF_URL]
|
||||
return self._async_create_entry_from_vars()
|
||||
|
||||
@ -172,11 +180,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle logic when on Supervisor host."""
|
||||
# Only one entry with Supervisor add-on support is allowed.
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.data.get(CONF_USE_ADDON):
|
||||
return await self.async_step_manual()
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA
|
||||
@ -289,7 +292,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
assert self.hass
|
||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||
try:
|
||||
await addon_manager.async_start_addon()
|
||||
await addon_manager.async_schedule_start_addon()
|
||||
# Sleep some seconds to let the add-on start properly before connecting.
|
||||
for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS):
|
||||
await asyncio.sleep(ADDON_SETUP_TIMEOUT)
|
||||
@ -338,7 +341,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
version_info.home_id, raise_on_progress=False
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_URL: self.ws_address,
|
||||
CONF_USB_PATH: self.usb_path,
|
||||
CONF_NETWORK_KEY: self.network_key,
|
||||
}
|
||||
)
|
||||
return self._async_create_entry_from_vars()
|
||||
|
||||
async def _async_get_addon_info(self) -> dict:
|
||||
@ -381,7 +390,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Install the Z-Wave JS add-on."""
|
||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||
try:
|
||||
await addon_manager.async_install_addon()
|
||||
await addon_manager.async_schedule_install_addon()
|
||||
finally:
|
||||
# Continue the flow after show progress when the task is done.
|
||||
self.hass.async_create_task(
|
||||
|
@ -169,7 +169,6 @@ class ZWaveBaseEntity(Entity):
|
||||
command_class: Optional[int] = None,
|
||||
endpoint: Optional[int] = None,
|
||||
value_property_key: Optional[int] = None,
|
||||
value_property_key_name: Optional[str] = None,
|
||||
add_to_watched_value_ids: bool = True,
|
||||
check_all_endpoints: bool = False,
|
||||
) -> Optional[ZwaveValue]:
|
||||
@ -188,7 +187,6 @@ class ZWaveBaseEntity(Entity):
|
||||
value_property,
|
||||
endpoint=endpoint,
|
||||
property_key=value_property_key,
|
||||
property_key_name=value_property_key_name,
|
||||
)
|
||||
return_value = self.info.node.values.get(value_id)
|
||||
|
||||
@ -203,7 +201,6 @@ class ZWaveBaseEntity(Entity):
|
||||
value_property,
|
||||
endpoint=endpoint_.index,
|
||||
property_key=value_property_key,
|
||||
property_key_name=value_property_key_name,
|
||||
)
|
||||
return_value = self.info.node.values.get(value_id)
|
||||
if return_value:
|
||||
|
@ -3,7 +3,6 @@ from typing import List, Tuple, cast
|
||||
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from zwave_js_server.model.value import Value as ZwaveValue
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@ -13,16 +12,6 @@ from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
|
||||
from .const import DATA_CLIENT, DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def get_old_value_id(value: ZwaveValue) -> str:
|
||||
"""Get old value ID so we can migrate entity unique ID."""
|
||||
command_class = value.command_class
|
||||
endpoint = value.endpoint or "00"
|
||||
property_ = value.property_
|
||||
property_key_name = value.property_key_name or "00"
|
||||
return f"{value.node.node_id}-{command_class}-{endpoint}-{property_}-{property_key_name}"
|
||||
|
||||
|
||||
@callback
|
||||
def get_unique_id(home_id: str, value_id: str) -> str:
|
||||
"""Get unique ID from home ID and value ID."""
|
||||
|
@ -228,7 +228,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
"targetColor",
|
||||
CommandClass.SWITCH_COLOR,
|
||||
value_property_key=None,
|
||||
value_property_key_name=None,
|
||||
)
|
||||
if combined_color_val and isinstance(combined_color_val.value, dict):
|
||||
colors_dict = {}
|
||||
@ -252,7 +251,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
"targetColor",
|
||||
CommandClass.SWITCH_COLOR,
|
||||
value_property_key=property_key.key,
|
||||
value_property_key_name=property_key.name,
|
||||
)
|
||||
if target_zwave_value is None:
|
||||
# guard for unsupported color
|
||||
@ -318,31 +316,26 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
"currentColor",
|
||||
CommandClass.SWITCH_COLOR,
|
||||
value_property_key=ColorComponent.RED.value.key,
|
||||
value_property_key_name=ColorComponent.RED.value.name,
|
||||
)
|
||||
green_val = self.get_zwave_value(
|
||||
"currentColor",
|
||||
CommandClass.SWITCH_COLOR,
|
||||
value_property_key=ColorComponent.GREEN.value.key,
|
||||
value_property_key_name=ColorComponent.GREEN.value.name,
|
||||
)
|
||||
blue_val = self.get_zwave_value(
|
||||
"currentColor",
|
||||
CommandClass.SWITCH_COLOR,
|
||||
value_property_key=ColorComponent.BLUE.value.key,
|
||||
value_property_key_name=ColorComponent.BLUE.value.name,
|
||||
)
|
||||
ww_val = self.get_zwave_value(
|
||||
"currentColor",
|
||||
CommandClass.SWITCH_COLOR,
|
||||
value_property_key=ColorComponent.WARM_WHITE.value.key,
|
||||
value_property_key_name=ColorComponent.WARM_WHITE.value.name,
|
||||
)
|
||||
cw_val = self.get_zwave_value(
|
||||
"currentColor",
|
||||
CommandClass.SWITCH_COLOR,
|
||||
value_property_key=ColorComponent.COLD_WHITE.value.key,
|
||||
value_property_key_name=ColorComponent.COLD_WHITE.value.name,
|
||||
)
|
||||
# prefer the (new) combined color property
|
||||
# https://github.com/zwave-js/node-zwave-js/pull/1782
|
||||
@ -350,7 +343,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
"currentColor",
|
||||
CommandClass.SWITCH_COLOR,
|
||||
value_property_key=None,
|
||||
value_property_key_name=None,
|
||||
)
|
||||
if combined_color_val and isinstance(combined_color_val.value, dict):
|
||||
multi_color = combined_color_val.value
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Z-Wave JS",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
||||
"requirements": ["zwave-js-server-python==0.20.1"],
|
||||
"requirements": ["zwave-js-server-python==0.21.0"],
|
||||
"codeowners": ["@home-assistant/z-wave"],
|
||||
"dependencies": ["http", "websocket_api"]
|
||||
}
|
||||
|
113
homeassistant/components/zwave_js/migrate.py
Normal file
113
homeassistant/components/zwave_js/migrate.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Functions used to migrate unique IDs for Z-Wave JS entities."""
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.model.value import Value as ZwaveValue
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
|
||||
from .const import DOMAIN
|
||||
from .discovery import ZwaveDiscoveryInfo
|
||||
from .helpers import get_unique_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_entity(
|
||||
ent_reg: EntityRegistry, platform: str, old_unique_id: str, new_unique_id: str
|
||||
) -> None:
|
||||
"""Check if entity with old unique ID exists, and if so migrate it to new ID."""
|
||||
if entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id):
|
||||
_LOGGER.debug(
|
||||
"Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
|
||||
entity_id,
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
try:
|
||||
ent_reg.async_update_entity(
|
||||
entity_id,
|
||||
new_unique_id=new_unique_id,
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Entity %s can't be migrated because the unique ID is taken. "
|
||||
"Cleaning it up since it is likely no longer valid."
|
||||
),
|
||||
entity_id,
|
||||
)
|
||||
ent_reg.async_remove(entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_discovered_value(
|
||||
ent_reg: EntityRegistry, client: ZwaveClient, disc_info: ZwaveDiscoveryInfo
|
||||
) -> None:
|
||||
"""Migrate unique ID for entity/entities tied to discovered value."""
|
||||
new_unique_id = get_unique_id(
|
||||
client.driver.controller.home_id,
|
||||
disc_info.primary_value.value_id,
|
||||
)
|
||||
|
||||
# 2021.2.*, 2021.3.0b0, and 2021.3.0 formats
|
||||
for value_id in get_old_value_ids(disc_info.primary_value):
|
||||
old_unique_id = get_unique_id(
|
||||
client.driver.controller.home_id,
|
||||
value_id,
|
||||
)
|
||||
# Most entities have the same ID format, but notification binary sensors
|
||||
# have a state key in their ID so we need to handle them differently
|
||||
if (
|
||||
disc_info.platform == "binary_sensor"
|
||||
and disc_info.platform_hint == "notification"
|
||||
):
|
||||
for state_key in disc_info.primary_value.metadata.states:
|
||||
# ignore idle key (0)
|
||||
if state_key == "0":
|
||||
continue
|
||||
|
||||
async_migrate_entity(
|
||||
ent_reg,
|
||||
disc_info.platform,
|
||||
f"{old_unique_id}.{state_key}",
|
||||
f"{new_unique_id}.{state_key}",
|
||||
)
|
||||
|
||||
# Once we've iterated through all state keys, we can move on to the
|
||||
# next item
|
||||
continue
|
||||
|
||||
async_migrate_entity(ent_reg, disc_info.platform, old_unique_id, new_unique_id)
|
||||
|
||||
|
||||
@callback
|
||||
def get_old_value_ids(value: ZwaveValue) -> List[str]:
|
||||
"""Get old value IDs so we can migrate entity unique ID."""
|
||||
value_ids = []
|
||||
|
||||
# Pre 2021.3.0 value ID
|
||||
command_class = value.command_class
|
||||
endpoint = value.endpoint or "00"
|
||||
property_ = value.property_
|
||||
property_key_name = value.property_key_name or "00"
|
||||
value_ids.append(
|
||||
f"{value.node.node_id}.{value.node.node_id}-{command_class}-{endpoint}-"
|
||||
f"{property_}-{property_key_name}"
|
||||
)
|
||||
|
||||
endpoint = "00" if value.endpoint is None else value.endpoint
|
||||
property_key = "00" if value.property_key is None else value.property_key
|
||||
property_key_name = value.property_key_name or "00"
|
||||
|
||||
value_id = (
|
||||
f"{value.node.node_id}-{command_class}-{endpoint}-"
|
||||
f"{property_}-{property_key}-{property_key_name}"
|
||||
)
|
||||
# 2021.3.0b0 and 2021.3.0 value IDs
|
||||
value_ids.extend([f"{value.node.node_id}.{value_id}", value_id])
|
||||
|
||||
return value_ids
|
@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 2021
|
||||
MINOR_VERSION = 3
|
||||
PATCH_VERSION = "0"
|
||||
PATCH_VERSION = "1"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 8, 0)
|
||||
|
@ -265,10 +265,9 @@ def async_numeric_state(
|
||||
"numeric_state", f"template error: {ex}"
|
||||
) from ex
|
||||
|
||||
# Known states that never match the numeric condition
|
||||
if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
raise ConditionErrorMessage(
|
||||
"numeric_state", f"state of {entity_id} is unavailable"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
fvalue = float(value)
|
||||
@ -281,13 +280,15 @@ def async_numeric_state(
|
||||
if below is not None:
|
||||
if isinstance(below, str):
|
||||
below_entity = hass.states.get(below)
|
||||
if not below_entity or below_entity.state in (
|
||||
if not below_entity:
|
||||
raise ConditionErrorMessage(
|
||||
"numeric_state", f"unknown 'below' entity {below}"
|
||||
)
|
||||
if below_entity.state in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
raise ConditionErrorMessage(
|
||||
"numeric_state", f"the 'below' entity {below} is unavailable"
|
||||
)
|
||||
return False
|
||||
try:
|
||||
if fvalue >= float(below_entity.state):
|
||||
return False
|
||||
@ -302,13 +303,15 @@ def async_numeric_state(
|
||||
if above is not None:
|
||||
if isinstance(above, str):
|
||||
above_entity = hass.states.get(above)
|
||||
if not above_entity or above_entity.state in (
|
||||
if not above_entity:
|
||||
raise ConditionErrorMessage(
|
||||
"numeric_state", f"unknown 'above' entity {above}"
|
||||
)
|
||||
if above_entity.state in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
raise ConditionErrorMessage(
|
||||
"numeric_state", f"the 'above' entity {above} is unavailable"
|
||||
)
|
||||
return False
|
||||
try:
|
||||
if fvalue <= float(above_entity.state):
|
||||
return False
|
||||
|
@ -15,7 +15,7 @@ defusedxml==0.6.0
|
||||
distro==1.5.0
|
||||
emoji==1.2.0
|
||||
hass-nabucasa==0.41.0
|
||||
home-assistant-frontend==20210302.3
|
||||
home-assistant-frontend==20210302.4
|
||||
httpx==0.16.1
|
||||
jinja2>=2.11.3
|
||||
netdisco==2.8.2
|
||||
|
@ -763,7 +763,7 @@ hole==0.5.1
|
||||
holidays==0.10.5.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210302.3
|
||||
home-assistant-frontend==20210302.4
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -2397,4 +2397,4 @@ zigpy==0.32.0
|
||||
zm-py==0.5.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.20.1
|
||||
zwave-js-server-python==0.21.0
|
||||
|
@ -412,7 +412,7 @@ hole==0.5.1
|
||||
holidays==0.10.5.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210302.3
|
||||
home-assistant-frontend==20210302.4
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -1234,4 +1234,4 @@ zigpy-znp==0.4.0
|
||||
zigpy==0.32.0
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.20.1
|
||||
zwave-js-server-python==0.21.0
|
||||
|
@ -10,12 +10,7 @@ import homeassistant.components.automation as automation
|
||||
from homeassistant.components.homeassistant.triggers import (
|
||||
numeric_state as numeric_state_trigger,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ENTITY_MATCH_ALL,
|
||||
SERVICE_TURN_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF
|
||||
from homeassistant.core import Context
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
@ -347,52 +342,6 @@ async def test_if_fires_on_entity_unavailable_at_startup(hass, calls):
|
||||
assert len(calls) == 0
|
||||
|
||||
|
||||
async def test_if_not_fires_on_entity_unavailable(hass, calls):
|
||||
"""Test the firing with entity changing to unavailable."""
|
||||
# set initial state
|
||||
hass.states.async_set("test.entity", 9)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
"platform": "numeric_state",
|
||||
"entity_id": "test.entity",
|
||||
"above": 10,
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# 11 is above 10
|
||||
hass.states.async_set("test.entity", 11)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
# Going to unavailable and back should not fire
|
||||
hass.states.async_set("test.entity", STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
hass.states.async_set("test.entity", 11)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
# Crossing threshold via unavailable should fire
|
||||
hass.states.async_set("test.entity", 9)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
hass.states.async_set("test.entity", STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
hass.states.async_set("test.entity", 11)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("above", (10, "input_number.value_10"))
|
||||
async def test_if_fires_on_entity_change_below_to_above(hass, calls, above):
|
||||
"""Test the firing with changed entity."""
|
||||
@ -1522,7 +1471,7 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls, above, below)
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
async def test_if_not_fires_on_error_with_for_template(hass, caplog, calls):
|
||||
async def test_if_not_fires_on_error_with_for_template(hass, calls):
|
||||
"""Test for not firing on error with for template."""
|
||||
hass.states.async_set("test.entity", 0)
|
||||
await hass.async_block_till_done()
|
||||
@ -1547,17 +1496,11 @@ async def test_if_not_fires_on_error_with_for_template(hass, caplog, calls):
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
caplog.clear()
|
||||
caplog.set_level(logging.WARNING)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
|
||||
hass.states.async_set("test.entity", "unavailable")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
assert len(caplog.record_tuples) == 1
|
||||
assert caplog.record_tuples[0][1] == logging.WARNING
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3))
|
||||
hass.states.async_set("test.entity", 101)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -100,41 +100,28 @@ async def test_static_config_with_invalid_host(hass):
|
||||
async def test_discovery(hass, pywemo_registry):
|
||||
"""Verify that discovery dispatches devices to the platform for setup."""
|
||||
|
||||
def create_device(uuid, location):
|
||||
def create_device(counter):
|
||||
"""Create a unique mock Motion detector device for each counter value."""
|
||||
device = create_autospec(pywemo.Motion, instance=True)
|
||||
device.host = location
|
||||
device.port = MOCK_PORT
|
||||
device.name = f"{MOCK_NAME}_{uuid}"
|
||||
device.serialnumber = f"{MOCK_SERIAL_NUMBER}_{uuid}"
|
||||
device.host = f"{MOCK_HOST}_{counter}"
|
||||
device.port = MOCK_PORT + counter
|
||||
device.name = f"{MOCK_NAME}_{counter}"
|
||||
device.serialnumber = f"{MOCK_SERIAL_NUMBER}_{counter}"
|
||||
device.model_name = "Motion"
|
||||
device.get_state.return_value = 0 # Default to Off
|
||||
return device
|
||||
|
||||
def create_upnp_entry(counter):
|
||||
return pywemo.ssdp.UPNPEntry.from_response(
|
||||
"\r\n".join(
|
||||
[
|
||||
"",
|
||||
f"LOCATION: http://192.168.1.100:{counter}/setup.xml",
|
||||
f"USN: uuid:Socket-1_0-SERIAL{counter}::upnp:rootdevice",
|
||||
"",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
upnp_entries = [create_upnp_entry(0), create_upnp_entry(1)]
|
||||
pywemo_devices = [create_device(0), create_device(1)]
|
||||
# Setup the component and start discovery.
|
||||
with patch(
|
||||
"pywemo.discovery.device_from_uuid_and_location", side_effect=create_device
|
||||
), patch("pywemo.ssdp.scan", return_value=upnp_entries) as mock_scan:
|
||||
"pywemo.discover_devices", return_value=pywemo_devices
|
||||
) as mock_discovery:
|
||||
assert await async_setup_component(
|
||||
hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}}
|
||||
)
|
||||
await pywemo_registry.semaphore.acquire() # Returns after platform setup.
|
||||
mock_scan.assert_called()
|
||||
# Add two of the same entries to test deduplication.
|
||||
upnp_entries.extend([create_upnp_entry(2), create_upnp_entry(2)])
|
||||
mock_discovery.assert_called()
|
||||
pywemo_devices.append(create_device(2))
|
||||
|
||||
# Test that discovery runs periodically and the async_dispatcher_send code works.
|
||||
async_fire_time_changed(
|
||||
|
@ -16,6 +16,7 @@ PROPERTY_DOOR_STATUS_BINARY_SENSOR = (
|
||||
CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat"
|
||||
CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat"
|
||||
CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat"
|
||||
CLIMATE_MAIN_HEAT_ACTIONNER = "climate.main_heat_actionner"
|
||||
BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color"
|
||||
EATON_RF9640_ENTITY = "light.allloaddimmer"
|
||||
AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6"
|
||||
|
@ -240,6 +240,12 @@ def nortek_thermostat_state_fixture():
|
||||
return json.loads(load_fixture("zwave_js/nortek_thermostat_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="srt321_hrt4_zw_state", scope="session")
|
||||
def srt321_hrt4_zw_state_fixture():
|
||||
"""Load the climate HRT4-ZW / SRT321 / SRT322 thermostat node state fixture data."""
|
||||
return json.loads(load_fixture("zwave_js/srt321_hrt4_zw_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="chain_actuator_zws12_state", scope="session")
|
||||
def window_cover_state_fixture():
|
||||
"""Load the window cover node state fixture data."""
|
||||
@ -417,6 +423,14 @@ def nortek_thermostat_fixture(client, nortek_thermostat_state):
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="srt321_hrt4_zw")
|
||||
def srt321_hrt4_zw_fixture(client, srt321_hrt4_zw_state):
|
||||
"""Mock a HRT4-ZW / SRT321 / SRT322 thermostat node."""
|
||||
node = Node(client, copy.deepcopy(srt321_hrt4_zw_state))
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="aeotec_radiator_thermostat")
|
||||
def aeotec_radiator_thermostat_fixture(client, aeotec_radiator_thermostat_state):
|
||||
"""Mock a Aeotec thermostat node."""
|
||||
|
@ -70,7 +70,7 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client):
|
||||
result = msg["result"]
|
||||
|
||||
assert len(result) == 61
|
||||
key = "52-112-0-2-00-00"
|
||||
key = "52-112-0-2"
|
||||
assert result[key]["property"] == 2
|
||||
assert result[key]["metadata"]["type"] == "number"
|
||||
assert result[key]["configuration_value_type"] == "enumerated"
|
||||
|
@ -31,6 +31,7 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
||||
from .common import (
|
||||
CLIMATE_DANFOSS_LC13_ENTITY,
|
||||
CLIMATE_FLOOR_THERMOSTAT_ENTITY,
|
||||
CLIMATE_MAIN_HEAT_ACTIONNER,
|
||||
CLIMATE_RADIO_THERMOSTAT_ENTITY,
|
||||
)
|
||||
|
||||
@ -404,7 +405,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
|
||||
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_HEAT
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 25
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 14
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT]
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
|
||||
|
||||
@ -431,6 +432,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
|
||||
"commandClassName": "Thermostat Setpoint",
|
||||
"property": "setpoint",
|
||||
"propertyName": "setpoint",
|
||||
"propertyKey": 1,
|
||||
"propertyKeyName": "Heating",
|
||||
"ccVersion": 2,
|
||||
"metadata": {
|
||||
@ -440,7 +442,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
|
||||
"unit": "\u00b0C",
|
||||
"ccSpecific": {"setpointType": 1},
|
||||
},
|
||||
"value": 25,
|
||||
"value": 14,
|
||||
}
|
||||
assert args["value"] == 21.5
|
||||
|
||||
@ -458,6 +460,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
|
||||
"commandClass": 67,
|
||||
"endpoint": 0,
|
||||
"property": "setpoint",
|
||||
"propertyKey": 1,
|
||||
"propertyKeyName": "Heating",
|
||||
"propertyName": "setpoint",
|
||||
"newValue": 23,
|
||||
@ -488,3 +491,19 @@ async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integratio
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 22.5
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
|
||||
|
||||
|
||||
async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integration):
|
||||
"""Test a climate entity from a HRT4-ZW / SRT321 thermostat device.
|
||||
|
||||
This device currently has no setpoint values.
|
||||
"""
|
||||
state = hass.states.get(CLIMATE_MAIN_HEAT_ACTIONNER)
|
||||
|
||||
assert state
|
||||
assert state.state == HVAC_MODE_OFF
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [
|
||||
HVAC_MODE_OFF,
|
||||
HVAC_MODE_HEAT,
|
||||
]
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None
|
||||
|
@ -184,7 +184,16 @@ async def test_manual_errors(
|
||||
|
||||
async def test_manual_already_configured(hass):
|
||||
"""Test that only one unique instance is allowed."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234)
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"url": "ws://localhost:3000",
|
||||
"use_addon": True,
|
||||
"integration_created_addon": True,
|
||||
},
|
||||
title=TITLE,
|
||||
unique_id=1234,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
@ -198,12 +207,15 @@ async def test_manual_already_configured(hass):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"url": "ws://localhost:3000",
|
||||
"url": "ws://1.1.1.1:3001",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.data["url"] == "ws://1.1.1.1:3001"
|
||||
assert entry.data["use_addon"] is False
|
||||
assert entry.data["integration_created_addon"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
|
||||
@ -507,49 +519,6 @@ async def test_not_addon(hass, supervisor):
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_addon_already_configured(hass, supervisor):
|
||||
"""Test add-on already configured leads to manual step."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={"use_addon": True}, title=TITLE, unique_id=5678
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "manual"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.zwave_js.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.zwave_js.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"url": "ws://localhost:3000",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TITLE
|
||||
assert result["data"] == {
|
||||
"url": "ws://localhost:3000",
|
||||
"usb_path": None,
|
||||
"network_key": None,
|
||||
"use_addon": False,
|
||||
"integration_created_addon": False,
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
|
||||
async def test_addon_running(
|
||||
hass,
|
||||
@ -661,9 +630,18 @@ async def test_addon_running_already_configured(
|
||||
hass, supervisor, addon_running, addon_options, get_addon_discovery_info
|
||||
):
|
||||
"""Test that only one unique instance is allowed when add-on is running."""
|
||||
addon_options["device"] = "/test"
|
||||
addon_options["network_key"] = "abc123"
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234)
|
||||
addon_options["device"] = "/test_new"
|
||||
addon_options["network_key"] = "def456"
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"url": "ws://localhost:3000",
|
||||
"usb_path": "/test",
|
||||
"network_key": "abc123",
|
||||
},
|
||||
title=TITLE,
|
||||
unique_id=1234,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
@ -680,6 +658,9 @@ async def test_addon_running_already_configured(
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.data["url"] == "ws://host1:3001"
|
||||
assert entry.data["usb_path"] == "/test_new"
|
||||
assert entry.data["network_key"] == "def456"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
|
||||
@ -885,7 +866,16 @@ async def test_addon_installed_already_configured(
|
||||
get_addon_discovery_info,
|
||||
):
|
||||
"""Test that only one unique instance is allowed when add-on is installed."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234)
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"url": "ws://localhost:3000",
|
||||
"usb_path": "/test",
|
||||
"network_key": "abc123",
|
||||
},
|
||||
title=TITLE,
|
||||
unique_id=1234,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
@ -904,7 +894,7 @@ async def test_addon_installed_already_configured(
|
||||
assert result["step_id"] == "configure_addon"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"usb_path": "/test", "network_key": "abc123"}
|
||||
result["flow_id"], {"usb_path": "/test_new", "network_key": "def456"}
|
||||
)
|
||||
|
||||
assert result["type"] == "progress"
|
||||
@ -915,6 +905,9 @@ async def test_addon_installed_already_configured(
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.data["url"] == "ws://host1:3001"
|
||||
assert entry.data["usb_path"] == "/test_new"
|
||||
assert entry.data["network_key"] == "def456"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}])
|
||||
|
@ -160,7 +160,7 @@ async def test_unique_id_migration_dupes(
|
||||
|
||||
# Check that new RegistryEntry is using new unique ID format
|
||||
entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
|
||||
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature-00-00"
|
||||
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
|
||||
assert ent_reg.async_get(f"{AIR_TEMPERATURE_SENSOR}_1") is None
|
||||
@ -195,7 +195,7 @@ async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integra
|
||||
|
||||
# Check that new RegistryEntry is using new unique ID format
|
||||
entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
|
||||
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature-00-00"
|
||||
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
|
||||
|
||||
@ -228,7 +228,147 @@ async def test_unique_id_migration_v2(hass, multisensor_6_state, client, integra
|
||||
|
||||
# Check that new RegistryEntry is using new unique ID format
|
||||
entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR)
|
||||
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance-00-00"
|
||||
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance"
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
|
||||
|
||||
async def test_unique_id_migration_v3(hass, multisensor_6_state, client, integration):
|
||||
"""Test unique ID is migrated from old format to new (version 3)."""
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
# Migrate version 2
|
||||
ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance"
|
||||
entity_name = ILLUMINANCE_SENSOR.split(".")[1]
|
||||
|
||||
# Create entity RegistryEntry using old unique ID format
|
||||
old_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance-00-00"
|
||||
entity_entry = ent_reg.async_get_or_create(
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
old_unique_id,
|
||||
suggested_object_id=entity_name,
|
||||
config_entry=integration,
|
||||
original_name=entity_name,
|
||||
)
|
||||
assert entity_entry.entity_id == ILLUMINANCE_SENSOR
|
||||
assert entity_entry.unique_id == old_unique_id
|
||||
|
||||
# Add a ready node, unique ID should be migrated
|
||||
node = Node(client, multisensor_6_state)
|
||||
event = {"node": node}
|
||||
|
||||
client.driver.controller.emit("node added", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that new RegistryEntry is using new unique ID format
|
||||
entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR)
|
||||
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance"
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
|
||||
|
||||
async def test_unique_id_migration_property_key_v1(
|
||||
hass, hank_binary_switch_state, client, integration
|
||||
):
|
||||
"""Test unique ID with property key is migrated from old format to new (version 1)."""
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
|
||||
SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
|
||||
entity_name = SENSOR_NAME.split(".")[1]
|
||||
|
||||
# Create entity RegistryEntry using old unique ID format
|
||||
old_unique_id = f"{client.driver.controller.home_id}.32.32-50-00-value-W_Consumed"
|
||||
entity_entry = ent_reg.async_get_or_create(
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
old_unique_id,
|
||||
suggested_object_id=entity_name,
|
||||
config_entry=integration,
|
||||
original_name=entity_name,
|
||||
)
|
||||
assert entity_entry.entity_id == SENSOR_NAME
|
||||
assert entity_entry.unique_id == old_unique_id
|
||||
|
||||
# Add a ready node, unique ID should be migrated
|
||||
node = Node(client, hank_binary_switch_state)
|
||||
event = {"node": node}
|
||||
|
||||
client.driver.controller.emit("node added", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that new RegistryEntry is using new unique ID format
|
||||
entity_entry = ent_reg.async_get(SENSOR_NAME)
|
||||
new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
|
||||
|
||||
async def test_unique_id_migration_property_key_v2(
|
||||
hass, hank_binary_switch_state, client, integration
|
||||
):
|
||||
"""Test unique ID with property key is migrated from old format to new (version 2)."""
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
|
||||
SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
|
||||
entity_name = SENSOR_NAME.split(".")[1]
|
||||
|
||||
# Create entity RegistryEntry using old unique ID format
|
||||
old_unique_id = (
|
||||
f"{client.driver.controller.home_id}.32.32-50-0-value-66049-W_Consumed"
|
||||
)
|
||||
entity_entry = ent_reg.async_get_or_create(
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
old_unique_id,
|
||||
suggested_object_id=entity_name,
|
||||
config_entry=integration,
|
||||
original_name=entity_name,
|
||||
)
|
||||
assert entity_entry.entity_id == SENSOR_NAME
|
||||
assert entity_entry.unique_id == old_unique_id
|
||||
|
||||
# Add a ready node, unique ID should be migrated
|
||||
node = Node(client, hank_binary_switch_state)
|
||||
event = {"node": node}
|
||||
|
||||
client.driver.controller.emit("node added", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that new RegistryEntry is using new unique ID format
|
||||
entity_entry = ent_reg.async_get(SENSOR_NAME)
|
||||
new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
|
||||
|
||||
async def test_unique_id_migration_property_key_v3(
|
||||
hass, hank_binary_switch_state, client, integration
|
||||
):
|
||||
"""Test unique ID with property key is migrated from old format to new (version 3)."""
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
|
||||
SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
|
||||
entity_name = SENSOR_NAME.split(".")[1]
|
||||
|
||||
# Create entity RegistryEntry using old unique ID format
|
||||
old_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049-W_Consumed"
|
||||
entity_entry = ent_reg.async_get_or_create(
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
old_unique_id,
|
||||
suggested_object_id=entity_name,
|
||||
config_entry=integration,
|
||||
original_name=entity_name,
|
||||
)
|
||||
assert entity_entry.entity_id == SENSOR_NAME
|
||||
assert entity_entry.unique_id == old_unique_id
|
||||
|
||||
# Add a ready node, unique ID should be migrated
|
||||
node = Node(client, hank_binary_switch_state)
|
||||
event = {"node": node}
|
||||
|
||||
client.driver.controller.emit("node added", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that new RegistryEntry is using new unique ID format
|
||||
entity_entry = ent_reg.async_get(SENSOR_NAME)
|
||||
new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
|
||||
|
||||
@ -262,7 +402,7 @@ async def test_unique_id_migration_notification_binary_sensor(
|
||||
|
||||
# Check that new RegistryEntry is using new unique ID format
|
||||
entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR)
|
||||
new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status-Motion sensor status.8"
|
||||
new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8"
|
||||
assert entity_entry.unique_id == new_unique_id
|
||||
|
||||
|
||||
@ -445,11 +585,13 @@ async def test_addon_info_failure(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"addon_version, update_available, update_calls, update_addon_side_effect",
|
||||
"addon_version, update_available, update_calls, snapshot_calls, "
|
||||
"update_addon_side_effect, create_shapshot_side_effect",
|
||||
[
|
||||
("1.0", True, 1, None),
|
||||
("1.0", False, 0, None),
|
||||
("1.0", True, 1, HassioAPIError("Boom")),
|
||||
("1.0", True, 1, 1, None, None),
|
||||
("1.0", False, 0, 0, None, None),
|
||||
("1.0", True, 1, 1, HassioAPIError("Boom"), None),
|
||||
("1.0", True, 0, 1, None, HassioAPIError("Boom")),
|
||||
],
|
||||
)
|
||||
async def test_update_addon(
|
||||
@ -464,11 +606,14 @@ async def test_update_addon(
|
||||
addon_version,
|
||||
update_available,
|
||||
update_calls,
|
||||
snapshot_calls,
|
||||
update_addon_side_effect,
|
||||
create_shapshot_side_effect,
|
||||
):
|
||||
"""Test update the Z-Wave JS add-on during entry setup."""
|
||||
addon_info.return_value["version"] = addon_version
|
||||
addon_info.return_value["update_available"] = update_available
|
||||
create_shapshot.side_effect = create_shapshot_side_effect
|
||||
update_addon.side_effect = update_addon_side_effect
|
||||
client.connect.side_effect = InvalidServerVersion("Invalid version")
|
||||
device = "/test"
|
||||
@ -490,12 +635,7 @@ async def test_update_addon(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
assert create_shapshot.call_count == 1
|
||||
assert create_shapshot.call_args == call(
|
||||
hass,
|
||||
{"name": f"addon_core_zwave_js_{addon_version}", "addons": ["core_zwave_js"]},
|
||||
partial=True,
|
||||
)
|
||||
assert create_shapshot.call_count == snapshot_calls
|
||||
assert update_addon.call_count == update_calls
|
||||
|
||||
|
||||
|
@ -4,11 +4,25 @@
|
||||
"status": 1,
|
||||
"ready": true,
|
||||
"deviceClass": {
|
||||
"basic": {"key": 4, "label":"Routing Slave"},
|
||||
"generic": {"key": 8, "label":"Thermostat"},
|
||||
"specific": {"key": 4, "label":"Setpoint Thermostat"},
|
||||
"mandatorySupportedCCs": [],
|
||||
"mandatoryControlCCs": []
|
||||
"basic": {
|
||||
"key": 4,
|
||||
"label": "Routing Slave"
|
||||
},
|
||||
"generic": {
|
||||
"key": 8,
|
||||
"label": "Thermostat"
|
||||
},
|
||||
"specific": {
|
||||
"key": 4,
|
||||
"label": "Setpoint Thermostat"
|
||||
},
|
||||
"mandatorySupportedCCs": [
|
||||
114,
|
||||
143,
|
||||
67,
|
||||
134
|
||||
],
|
||||
"mandatoryControlledCCs": []
|
||||
},
|
||||
"isListening": false,
|
||||
"isFrequentListening": false,
|
||||
@ -22,6 +36,7 @@
|
||||
"productType": 5,
|
||||
"firmwareVersion": "1.1",
|
||||
"deviceConfig": {
|
||||
"filename": "/usr/src/app/node_modules/@zwave-js/config/config/devices/0x0002/lc-13.json",
|
||||
"manufacturerId": 2,
|
||||
"manufacturer": "Danfoss",
|
||||
"label": "LC-13",
|
||||
@ -66,19 +81,76 @@
|
||||
14
|
||||
],
|
||||
"interviewAttempts": 1,
|
||||
"interviewStage": 7,
|
||||
"commandClasses": [
|
||||
{
|
||||
"id": 67,
|
||||
"name": "Thermostat Setpoint",
|
||||
"version": 2,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 70,
|
||||
"name": "Climate Control Schedule",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 114,
|
||||
"name": "Manufacturer Specific",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 117,
|
||||
"name": "Protection",
|
||||
"version": 2,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 128,
|
||||
"name": "Battery",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 129,
|
||||
"name": "Clock",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 132,
|
||||
"name": "Wake Up",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 134,
|
||||
"name": "Version",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 143,
|
||||
"name": "Multi Command",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
}
|
||||
],
|
||||
"endpoints": [
|
||||
{
|
||||
"nodeId": 5,
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"commandClasses": [],
|
||||
"values": [
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 67,
|
||||
"commandClassName": "Thermostat Setpoint",
|
||||
"property": "setpoint",
|
||||
"propertyKey": 1,
|
||||
"propertyName": "setpoint",
|
||||
"propertyKeyName": "Heating",
|
||||
"ccVersion": 2,
|
||||
@ -91,7 +163,7 @@
|
||||
"setpointType": 1
|
||||
}
|
||||
},
|
||||
"value": 25
|
||||
"value": 14
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
@ -262,7 +334,7 @@
|
||||
"unit": "%",
|
||||
"label": "Battery level"
|
||||
},
|
||||
"value": 53
|
||||
"value": 49
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
@ -361,4 +433,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -837,6 +837,7 @@
|
||||
"commandClassName": "Thermostat Setpoint",
|
||||
"property": "setpoint",
|
||||
"propertyName": "setpoint",
|
||||
"propertyKey": 1,
|
||||
"propertyKeyName": "Heating",
|
||||
"ccVersion": 3,
|
||||
"metadata": {
|
||||
|
262
tests/fixtures/zwave_js/srt321_hrt4_zw_state.json
vendored
Normal file
262
tests/fixtures/zwave_js/srt321_hrt4_zw_state.json
vendored
Normal file
@ -0,0 +1,262 @@
|
||||
{
|
||||
"nodeId": 20,
|
||||
"index": 0,
|
||||
"status": 4,
|
||||
"ready": true,
|
||||
"deviceClass": {
|
||||
"basic": {
|
||||
"key": 4,
|
||||
"label": "Routing Slave"
|
||||
},
|
||||
"generic": {
|
||||
"key": 8,
|
||||
"label": "Thermostat"
|
||||
},
|
||||
"specific": {
|
||||
"key": 0,
|
||||
"label": "Unused"
|
||||
},
|
||||
"mandatorySupportedCCs": [],
|
||||
"mandatoryControlledCCs": []
|
||||
},
|
||||
"isListening": true,
|
||||
"isFrequentListening": false,
|
||||
"isRouting": true,
|
||||
"maxBaudRate": 40000,
|
||||
"isSecure": false,
|
||||
"version": 3,
|
||||
"isBeaming": true,
|
||||
"manufacturerId": 89,
|
||||
"productId": 1,
|
||||
"productType": 3,
|
||||
"firmwareVersion": "2.0",
|
||||
"name": "main_heat_actionner",
|
||||
"location": "kitchen",
|
||||
"deviceConfig": {
|
||||
"filename": "/opt/node_modules/@zwave-js/config/config/devices/0x0059/asr-zw.json",
|
||||
"manufacturerId": 89,
|
||||
"manufacturer": "Secure Meters (UK) Ltd.",
|
||||
"label": "SRT322",
|
||||
"description": "Thermostat Receiver",
|
||||
"devices": [
|
||||
{
|
||||
"productType": "0x0003",
|
||||
"productId": "0x0001"
|
||||
}
|
||||
],
|
||||
"firmwareVersion": {
|
||||
"min": "0.0",
|
||||
"max": "255.255"
|
||||
}
|
||||
},
|
||||
"label": "SRT322",
|
||||
"neighbors": [
|
||||
1,
|
||||
5,
|
||||
10,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
18,
|
||||
21
|
||||
],
|
||||
"interviewAttempts": 1,
|
||||
"interviewStage": 7,
|
||||
"commandClasses": [
|
||||
{
|
||||
"id": 37,
|
||||
"name": "Binary Switch",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"name": "Thermostat Mode",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 114,
|
||||
"name": "Manufacturer Specific",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
},
|
||||
{
|
||||
"id": 134,
|
||||
"name": "Version",
|
||||
"version": 1,
|
||||
"isSecure": false
|
||||
}
|
||||
],
|
||||
"endpoints": [
|
||||
{
|
||||
"nodeId": 20,
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"values": [
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 37,
|
||||
"commandClassName": "Binary Switch",
|
||||
"property": "currentValue",
|
||||
"propertyName": "currentValue",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "boolean",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Current value"
|
||||
},
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 37,
|
||||
"commandClassName": "Binary Switch",
|
||||
"property": "targetValue",
|
||||
"propertyName": "targetValue",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "boolean",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"label": "Target value"
|
||||
},
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 64,
|
||||
"commandClassName": "Thermostat Mode",
|
||||
"property": "mode",
|
||||
"propertyName": "mode",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": true,
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "Off",
|
||||
"1": "Heat"
|
||||
},
|
||||
"label": "Thermostat mode"
|
||||
},
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 64,
|
||||
"commandClassName": "Thermostat Mode",
|
||||
"property": "manufacturerData",
|
||||
"propertyName": "manufacturerData",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 114,
|
||||
"commandClassName": "Manufacturer Specific",
|
||||
"property": "manufacturerId",
|
||||
"propertyName": "manufacturerId",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"min": 0,
|
||||
"max": 65535,
|
||||
"label": "Manufacturer ID"
|
||||
},
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 114,
|
||||
"commandClassName": "Manufacturer Specific",
|
||||
"property": "productType",
|
||||
"propertyName": "productType",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"min": 0,
|
||||
"max": 65535,
|
||||
"label": "Product type"
|
||||
},
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 114,
|
||||
"commandClassName": "Manufacturer Specific",
|
||||
"property": "productId",
|
||||
"propertyName": "productId",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"min": 0,
|
||||
"max": 65535,
|
||||
"label": "Product ID"
|
||||
},
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "libraryType",
|
||||
"propertyName": "libraryType",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Library type"
|
||||
},
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "protocolVersion",
|
||||
"propertyName": "protocolVersion",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Z-Wave protocol version"
|
||||
},
|
||||
"value": "2.78"
|
||||
},
|
||||
{
|
||||
"endpoint": 0,
|
||||
"commandClass": 134,
|
||||
"commandClassName": "Version",
|
||||
"property": "firmwareVersions",
|
||||
"propertyName": "firmwareVersions",
|
||||
"ccVersion": 1,
|
||||
"metadata": {
|
||||
"type": "any",
|
||||
"readable": true,
|
||||
"writeable": false,
|
||||
"label": "Z-Wave chip firmware versions"
|
||||
},
|
||||
"value": [
|
||||
"2.0"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
"""Test the condition helper."""
|
||||
from logging import WARNING
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@ -363,27 +362,6 @@ async def test_time_using_input_datetime(hass):
|
||||
condition.time(hass, before="input_datetime.not_existing")
|
||||
|
||||
|
||||
async def test_if_numeric_state_raises_on_unavailable(hass, caplog):
|
||||
"""Test numeric_state raises on unavailable/unknown state."""
|
||||
test = await condition.async_from_config(
|
||||
hass,
|
||||
{"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42},
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
caplog.set_level(WARNING)
|
||||
|
||||
hass.states.async_set("sensor.temperature", "unavailable")
|
||||
with pytest.raises(ConditionError):
|
||||
test(hass)
|
||||
assert len(caplog.record_tuples) == 0
|
||||
|
||||
hass.states.async_set("sensor.temperature", "unknown")
|
||||
with pytest.raises(ConditionError):
|
||||
test(hass)
|
||||
assert len(caplog.record_tuples) == 0
|
||||
|
||||
|
||||
async def test_state_raises(hass):
|
||||
"""Test that state raises ConditionError on errors."""
|
||||
# No entity
|
||||
@ -631,6 +609,26 @@ async def test_state_using_input_entities(hass):
|
||||
assert test(hass)
|
||||
|
||||
|
||||
async def test_numeric_state_known_non_matching(hass):
|
||||
"""Test that numeric_state doesn't match on known non-matching states."""
|
||||
hass.states.async_set("sensor.temperature", "unavailable")
|
||||
test = await condition.async_from_config(
|
||||
hass,
|
||||
{
|
||||
"condition": "numeric_state",
|
||||
"entity_id": "sensor.temperature",
|
||||
"above": 0,
|
||||
},
|
||||
)
|
||||
|
||||
# Unavailable state
|
||||
assert not test(hass)
|
||||
|
||||
# Unknown state
|
||||
hass.states.async_set("sensor.temperature", "unknown")
|
||||
assert not test(hass)
|
||||
|
||||
|
||||
async def test_numeric_state_raises(hass):
|
||||
"""Test that numeric_state raises ConditionError on errors."""
|
||||
# Unknown entities
|
||||
@ -677,20 +675,6 @@ async def test_numeric_state_raises(hass):
|
||||
hass.states.async_set("sensor.temperature", 50)
|
||||
test(hass)
|
||||
|
||||
# Unavailable state
|
||||
with pytest.raises(ConditionError, match="state of .* is unavailable"):
|
||||
test = await condition.async_from_config(
|
||||
hass,
|
||||
{
|
||||
"condition": "numeric_state",
|
||||
"entity_id": "sensor.temperature",
|
||||
"above": 0,
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("sensor.temperature", "unavailable")
|
||||
test(hass)
|
||||
|
||||
# Bad number
|
||||
with pytest.raises(ConditionError, match="cannot be processed as a number"):
|
||||
test = await condition.async_from_config(
|
||||
@ -852,6 +836,12 @@ async def test_numeric_state_using_input_number(hass):
|
||||
hass.states.async_set("sensor.temperature", 100)
|
||||
assert not test(hass)
|
||||
|
||||
hass.states.async_set("input_number.high", "unknown")
|
||||
assert not test(hass)
|
||||
|
||||
hass.states.async_set("input_number.high", "unavailable")
|
||||
assert not test(hass)
|
||||
|
||||
await hass.services.async_call(
|
||||
"input_number",
|
||||
"set_value",
|
||||
@ -863,6 +853,12 @@ async def test_numeric_state_using_input_number(hass):
|
||||
)
|
||||
assert test(hass)
|
||||
|
||||
hass.states.async_set("input_number.low", "unknown")
|
||||
assert not test(hass)
|
||||
|
||||
hass.states.async_set("input_number.low", "unavailable")
|
||||
assert not test(hass)
|
||||
|
||||
with pytest.raises(ConditionError):
|
||||
condition.async_numeric_state(
|
||||
hass, entity="sensor.temperature", below="input_number.not_exist"
|
||||
|
Loading…
x
Reference in New Issue
Block a user