Merge pull request #46249 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-02-09 00:28:15 +01:00 committed by GitHub
commit 0ddc7d90a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 540 additions and 210 deletions

View File

@ -48,8 +48,11 @@ async def async_migrate_entry(hass, config_entry):
# Flatten configuration but keep old data if user rollbacks HASS prior to 0.106
if config_entry.version == 1:
config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
config_entry.unique_id = config_entry.data[CONF_MAC]
unique_id = config_entry.data[CONF_MAC]
data = {**config_entry.data, **config_entry.data[CONF_DEVICE]}
hass.config_entries.async_update_entry(
config_entry, unique_id=unique_id, data=data
)
config_entry.version = 2
# Normalise MAC address of device which also affects entity unique IDs
@ -66,10 +69,12 @@ async def async_migrate_entry(hass, config_entry):
)
}
await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
if old_unique_id != new_unique_id:
await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
config_entry.unique_id = new_unique_id
config_entry.version = 3
hass.config_entries.async_update_entry(
config_entry, unique_id=new_unique_id
)
_LOGGER.info("Migration to version %s successful", config_entry.version)

View File

@ -6,7 +6,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import Event
from .const import DOMAIN as DECONZ_DOMAIN
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
from .deconz_event import CONF_DECONZ_EVENT, DeconzEvent
from .device_trigger import (
CONF_BOTH_BUTTONS,
@ -107,8 +107,12 @@ def _get_device_event_description(modelid: str, event: str) -> tuple:
device_event_descriptions: dict = REMOTES[modelid]
for event_type_tuple, event_dict in device_event_descriptions.items():
if event == event_dict[CONF_EVENT]:
if event == event_dict.get(CONF_EVENT):
return event_type_tuple
if event == event_dict.get(CONF_GESTURE):
return event_type_tuple
return (None, None)
@callback
@ -125,15 +129,35 @@ def async_describe_events(
hass, event.data[ATTR_DEVICE_ID]
)
if deconz_event.device.modelid not in REMOTES:
action = None
interface = None
data = event.data.get(CONF_EVENT) or event.data.get(CONF_GESTURE, "")
if data and deconz_event.device.modelid in REMOTES:
action, interface = _get_device_event_description(
deconz_event.device.modelid, data
)
# Unknown event
if not data:
return {
"name": f"{deconz_event.device.name}",
"message": f"fired event '{event.data[CONF_EVENT]}'.",
"message": "fired an unknown event.",
}
action, interface = _get_device_event_description(
deconz_event.device.modelid, event.data[CONF_EVENT]
)
# No device event match
if not action:
return {
"name": f"{deconz_event.device.name}",
"message": f"fired event '{data}'.",
}
# Gesture event
if not interface:
return {
"name": f"{deconz_event.device.name}",
"message": f"fired event '{ACTIONS[action]}'.",
}
return {
"name": f"{deconz_event.device.name}",

View File

@ -70,8 +70,9 @@ def setup(hass, config):
overwrite = service.data.get(ATTR_OVERWRITE)
# Check the path
raise_if_invalid_path(subdir)
if subdir:
# Check the path
raise_if_invalid_path(subdir)
final_path = None

View File

@ -1,10 +1,15 @@
"""The foscam component."""
import asyncio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from libpyfoscam import FoscamCamera
from .const import DOMAIN, SERVICE_PTZ, SERVICE_PTZ_PRESET
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_registry import async_migrate_entries
from .config_flow import DEFAULT_RTSP_PORT
from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET
PLATFORMS = ["camera"]
@ -22,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.config_entries.async_forward_entry_setup(entry, component)
)
hass.data[DOMAIN][entry.unique_id] = entry.data
hass.data[DOMAIN][entry.entry_id] = entry.data
return True
@ -39,10 +44,50 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
)
if unload_ok:
hass.data[DOMAIN].pop(entry.unique_id)
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ_PRESET)
return unload_ok
async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1:
# Change unique id
@callback
def update_unique_id(entry):
return {"new_unique_id": config_entry.entry_id}
await async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
config_entry.unique_id = None
# Get RTSP port from the camera or use the fallback one and store it in data
camera = FoscamCamera(
config_entry.data[CONF_HOST],
config_entry.data[CONF_PORT],
config_entry.data[CONF_USERNAME],
config_entry.data[CONF_PASSWORD],
verbose=False,
)
ret, response = await hass.async_add_executor_job(camera.get_port_info)
rtsp_port = DEFAULT_RTSP_PORT
if ret != 0:
rtsp_port = response.get("rtspPort") or response.get("mediaPort")
config_entry.data = {**config_entry.data, CONF_RTSP_PORT: rtsp_port}
# Change entry version
config_entry.version = 2
LOGGER.info("Migration to version %s successful", config_entry.version)
return True

View File

@ -15,7 +15,14 @@ from homeassistant.const import (
)
from homeassistant.helpers import config_validation as cv, entity_platform
from .const import CONF_STREAM, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET
from .const import (
CONF_RTSP_PORT,
CONF_STREAM,
DOMAIN,
LOGGER,
SERVICE_PTZ,
SERVICE_PTZ_PRESET,
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@ -24,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string,
vol.Optional(CONF_PORT, default=88): cv.port,
vol.Optional("rtsp_port"): cv.port,
vol.Optional(CONF_RTSP_PORT): cv.port,
}
)
@ -71,6 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
CONF_USERNAME: config[CONF_USERNAME],
CONF_PASSWORD: config[CONF_PASSWORD],
CONF_STREAM: "Main",
CONF_RTSP_PORT: config.get(CONF_RTSP_PORT, 554),
}
hass.async_create_task(
@ -134,8 +142,8 @@ class HassFoscamCamera(Camera):
self._username = config_entry.data[CONF_USERNAME]
self._password = config_entry.data[CONF_PASSWORD]
self._stream = config_entry.data[CONF_STREAM]
self._unique_id = config_entry.unique_id
self._rtsp_port = None
self._unique_id = config_entry.entry_id
self._rtsp_port = config_entry.data[CONF_RTSP_PORT]
self._motion_status = False
async def async_added_to_hass(self):
@ -145,7 +153,13 @@ class HassFoscamCamera(Camera):
self._foscam_session.get_motion_detect_config
)
if ret != 0:
if ret == -3:
LOGGER.info(
"Can't get motion detection status, camera %s configured with non-admin user",
self._name,
)
elif ret != 0:
LOGGER.error(
"Error getting motion detection status of %s: %s", self._name, ret
)
@ -153,17 +167,6 @@ class HassFoscamCamera(Camera):
else:
self._motion_status = response == 1
# Get RTSP port
ret, response = await self.hass.async_add_executor_job(
self._foscam_session.get_port_info
)
if ret != 0:
LOGGER.error("Error getting RTSP port of %s: %s", self._name, ret)
else:
self._rtsp_port = response.get("rtspPort") or response.get("mediaPort")
@property
def unique_id(self):
"""Return the entity unique ID."""
@ -205,6 +208,11 @@ class HassFoscamCamera(Camera):
ret = self._foscam_session.enable_motion_detection()
if ret != 0:
if ret == -3:
LOGGER.info(
"Can't set motion detection status, camera %s configured with non-admin user",
self._name,
)
return
self._motion_status = True
@ -220,6 +228,11 @@ class HassFoscamCamera(Camera):
ret = self._foscam_session.disable_motion_detection()
if ret != 0:
if ret == -3:
LOGGER.info(
"Can't set motion detection status, camera %s configured with non-admin user",
self._name,
)
return
self._motion_status = False

View File

@ -1,6 +1,10 @@
"""Config flow for foscam integration."""
from libpyfoscam import FoscamCamera
from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE
from libpyfoscam.foscam import (
ERROR_FOSCAM_AUTH,
ERROR_FOSCAM_UNAVAILABLE,
FOSCAM_SUCCESS,
)
import voluptuous as vol
from homeassistant import config_entries, exceptions
@ -13,12 +17,13 @@ from homeassistant.const import (
)
from homeassistant.data_entry_flow import AbortFlow
from .const import CONF_STREAM, LOGGER
from .const import CONF_RTSP_PORT, CONF_STREAM, LOGGER
from .const import DOMAIN # pylint:disable=unused-import
STREAMS = ["Main", "Sub"]
DEFAULT_PORT = 88
DEFAULT_RTSP_PORT = 554
DATA_SCHEMA = vol.Schema(
@ -28,6 +33,7 @@ DATA_SCHEMA = vol.Schema(
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_STREAM, default=STREAMS[0]): vol.In(STREAMS),
vol.Required(CONF_RTSP_PORT, default=DEFAULT_RTSP_PORT): int,
}
)
@ -35,7 +41,7 @@ DATA_SCHEMA = vol.Schema(
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for foscam."""
VERSION = 1
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def _validate_and_create(self, data):
@ -43,6 +49,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
for entry in self.hass.config_entries.async_entries(DOMAIN):
if (
entry.data[CONF_HOST] == data[CONF_HOST]
and entry.data[CONF_PORT] == data[CONF_PORT]
):
raise AbortFlow("already_configured")
camera = FoscamCamera(
data[CONF_HOST],
data[CONF_PORT],
@ -52,7 +66,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
)
# Validate data by sending a request to the camera
ret, response = await self.hass.async_add_executor_job(camera.get_dev_info)
ret, _ = await self.hass.async_add_executor_job(camera.get_product_all_info)
if ret == ERROR_FOSCAM_UNAVAILABLE:
raise CannotConnect
@ -60,10 +74,23 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if ret == ERROR_FOSCAM_AUTH:
raise InvalidAuth
await self.async_set_unique_id(response["mac"])
self._abort_if_unique_id_configured()
if ret != FOSCAM_SUCCESS:
LOGGER.error(
"Unexpected error code from camera %s:%s: %s",
data[CONF_HOST],
data[CONF_PORT],
ret,
)
raise InvalidResponse
name = data.pop(CONF_NAME, response["devName"])
# Try to get camera name (only possible with admin account)
ret, response = await self.hass.async_add_executor_job(camera.get_dev_info)
dev_name = response.get(
"devName", f"Foscam {data[CONF_HOST]}:{data[CONF_PORT]}"
)
name = data.pop(CONF_NAME, dev_name)
return self.async_create_entry(title=name, data=data)
@ -81,6 +108,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except InvalidAuth:
errors["base"] = "invalid_auth"
except InvalidResponse:
errors["base"] = "invalid_response"
except AbortFlow:
raise
@ -105,6 +135,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
LOGGER.error("Error importing foscam platform config: invalid auth.")
return self.async_abort(reason="invalid_auth")
except InvalidResponse:
LOGGER.exception(
"Error importing foscam platform config: invalid response from camera."
)
return self.async_abort(reason="invalid_response")
except AbortFlow:
raise
@ -121,3 +157,7 @@ class CannotConnect(exceptions.HomeAssistantError):
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
class InvalidResponse(exceptions.HomeAssistantError):
"""Error to indicate there is invalid response."""

View File

@ -5,6 +5,7 @@ LOGGER = logging.getLogger(__package__)
DOMAIN = "foscam"
CONF_RTSP_PORT = "rtsp_port"
CONF_STREAM = "stream"
SERVICE_PTZ = "ptz"

View File

@ -8,6 +8,7 @@
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"rtsp_port": "RTSP port",
"stream": "Stream"
}
}
@ -15,6 +16,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_response": "Invalid response from the device",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {

View File

@ -6,6 +6,7 @@
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"invalid_response": "Invalid response from the device",
"unknown": "Unexpected error"
},
"step": {
@ -14,6 +15,7 @@
"host": "Host",
"password": "Password",
"port": "Port",
"rtsp_port": "RTSP port",
"stream": "Stream",
"username": "Username"
}

View File

@ -2,6 +2,6 @@
"domain": "google_translate",
"name": "Google Translate Text-to-Speech",
"documentation": "https://www.home-assistant.io/integrations/google_translate",
"requirements": ["gTTS==2.2.1"],
"requirements": ["gTTS==2.2.2"],
"codeowners": []
}

View File

@ -595,10 +595,15 @@ class WaterHeater(HomeAccessory):
def async_update_state(self, new_state):
"""Update water_heater state after state change."""
# Update current and target temperature
temperature = _get_target_temperature(new_state, self._unit)
if temperature is not None:
if temperature != self.char_current_temp.value:
self.char_target_temp.set_value(temperature)
target_temperature = _get_target_temperature(new_state, self._unit)
if target_temperature is not None:
if target_temperature != self.char_target_temp.value:
self.char_target_temp.set_value(target_temperature)
current_temperature = _get_current_temperature(new_state, self._unit)
if current_temperature is not None:
if current_temperature != self.char_current_temp.value:
self.char_current_temp.set_value(current_temperature)
# Update display units
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:

View File

@ -290,7 +290,7 @@ class KNXModule:
if CONF_KNX_ROUTING in self.config[DOMAIN]:
return self.connection_config_routing()
# config from xknx.yaml always has priority later on
return ConnectionConfig()
return ConnectionConfig(auto_reconnect=True)
def connection_config_routing(self):
"""Return the connection_config if routing is configured."""

View File

@ -28,7 +28,6 @@ from homeassistant.components.climate.const import (
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.temperature import convert as convert_temperature
from .const import DATA_UNSUBSCRIBE, DOMAIN
from .entity import ZWaveDeviceEntity
@ -155,13 +154,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
def convert_units(units):
"""Return units as a farenheit or celsius constant."""
if units == "F":
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
"""Representation of a Z-Wave Climate device."""
@ -207,18 +199,16 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return convert_units(self._current_mode_setpoint_values[0].units)
if self.values.temperature is not None and self.values.temperature.units == "F":
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
if not self.values.temperature:
return None
return convert_temperature(
self.values.temperature.value,
convert_units(self._current_mode_setpoint_values[0].units),
self.temperature_unit,
)
return self.values.temperature.value
@property
def hvac_action(self):
@ -246,29 +236,17 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return convert_temperature(
self._current_mode_setpoint_values[0].value,
convert_units(self._current_mode_setpoint_values[0].units),
self.temperature_unit,
)
return self._current_mode_setpoint_values[0].value
@property
def target_temperature_low(self) -> Optional[float]:
"""Return the lowbound target temperature we try to reach."""
return convert_temperature(
self._current_mode_setpoint_values[0].value,
convert_units(self._current_mode_setpoint_values[0].units),
self.temperature_unit,
)
return self._current_mode_setpoint_values[0].value
@property
def target_temperature_high(self) -> Optional[float]:
"""Return the highbound target temperature we try to reach."""
return convert_temperature(
self._current_mode_setpoint_values[1].value,
convert_units(self._current_mode_setpoint_values[1].units),
self.temperature_unit,
)
return self._current_mode_setpoint_values[1].value
async def async_set_temperature(self, **kwargs):
"""Set new target temperature.
@ -284,29 +262,14 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
setpoint = self._current_mode_setpoint_values[0]
target_temp = kwargs.get(ATTR_TEMPERATURE)
if setpoint is not None and target_temp is not None:
target_temp = convert_temperature(
target_temp,
self.temperature_unit,
convert_units(setpoint.units),
)
setpoint.send_value(target_temp)
elif len(self._current_mode_setpoint_values) == 2:
(setpoint_low, setpoint_high) = self._current_mode_setpoint_values
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if setpoint_low is not None and target_temp_low is not None:
target_temp_low = convert_temperature(
target_temp_low,
self.temperature_unit,
convert_units(setpoint_low.units),
)
setpoint_low.send_value(target_temp_low)
if setpoint_high is not None and target_temp_high is not None:
target_temp_high = convert_temperature(
target_temp_high,
self.temperature_unit,
convert_units(setpoint_high.units),
)
setpoint_high.send_value(target_temp_high)
async def async_set_fan_mode(self, fan_mode):

View File

@ -3,7 +3,12 @@ import logging
from sqlalchemy import ForeignKeyConstraint, MetaData, Table, text
from sqlalchemy.engine import reflection
from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError
from sqlalchemy.exc import (
InternalError,
OperationalError,
ProgrammingError,
SQLAlchemyError,
)
from sqlalchemy.schema import AddConstraint, DropConstraint
from .const import DOMAIN
@ -69,7 +74,7 @@ def _create_index(engine, table_name, index_name):
)
try:
index.create(engine)
except OperationalError as err:
except (InternalError, ProgrammingError, OperationalError) as err:
lower_err_str = str(err).lower()
if "already exists" not in lower_err_str and "duplicate" not in lower_err_str:
@ -78,13 +83,6 @@ def _create_index(engine, table_name, index_name):
_LOGGER.warning(
"Index %s already exists on %s, continuing", index_name, table_name
)
except InternalError as err:
if "duplicate" not in str(err).lower():
raise
_LOGGER.warning(
"Index %s already exists on %s, continuing", index_name, table_name
)
_LOGGER.debug("Finished creating %s", index_name)

View File

@ -108,6 +108,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
)
return False
if "result" not in mylink_status:
raise ConfigEntryNotReady("The Somfy MyLink device returned an empty result")
_async_migrate_entity_config(hass, entry, mylink_status)
undo_listener = entry.add_update_listener(_async_update_listener)

View File

@ -244,10 +244,10 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity):
return
if self.zone_variable == "power":
self._state = self._tado_zone_data.power
self._state = self._tado_zone_data.power == "ON"
elif self.zone_variable == "link":
self._state = self._tado_zone_data.link
self._state = self._tado_zone_data.link == "ONLINE"
elif self.zone_variable == "overlay":
self._state = self._tado_zone_data.overlay_active

View File

@ -175,7 +175,7 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity):
self._refreshing = True
self.values.primary.refresh()
if self._timer is not None and self._timer.isAlive():
if self._timer is not None and self._timer.is_alive():
self._timer.cancel()
self._timer = Timer(self._delay, _refresh_value)

View File

@ -293,6 +293,10 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Initialize a ZWaveNotificationBinarySensor entity."""
super().__init__(config_entry, client, info)
self.state_key = state_key
self._name = self.generate_name(
self.info.primary_value.property_name,
[self.info.primary_value.metadata.states[self.state_key]],
)
# check if we have a custom mapping for this value
self._mapping_info = self._get_sensor_mapping()
@ -301,14 +305,6 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Return if the sensor is on or off."""
return int(self.info.primary_value.value) == int(self.state_key)
@property
def name(self) -> str:
"""Return default name from device name and value name combination."""
node_name = self.info.node.name or self.info.node.device_config.description
value_name = self.info.primary_value.property_name
state_label = self.info.primary_value.metadata.states[self.state_key]
return f"{node_name}: {value_name} - {state_label}"
@property
def device_class(self) -> Optional[str]:
"""Return device class."""

View File

@ -207,6 +207,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
if self._current_mode is None:
# Thermostat(valve) with no support for setting a mode is considered heating-only
return HVAC_MODE_HEAT
if self._current_mode.value is None:
# guard missing value
return HVAC_MODE_HEAT
return ZW_HVAC_MODE_MAP.get(int(self._current_mode.value), HVAC_MODE_HEAT_COOL)
@property
@ -219,6 +222,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
"""Return the current running hvac operation if supported."""
if not self._operating_state:
return None
if self._operating_state.value is None:
# guard missing value
return None
return HVAC_CURRENT_MAP.get(int(self._operating_state.value))
@property
@ -234,12 +240,18 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@property
def target_temperature(self) -> Optional[float]:
"""Return the temperature we try to reach."""
if self._current_mode and self._current_mode.value is None:
# guard missing value
return None
temp = self._setpoint_value(self._current_mode_setpoint_enums[0])
return temp.value if temp else None
@property
def target_temperature_high(self) -> Optional[float]:
"""Return the highbound target temperature we try to reach."""
if self._current_mode and self._current_mode.value is None:
# guard missing value
return None
temp = self._setpoint_value(self._current_mode_setpoint_enums[1])
return temp.value if temp else None
@ -251,6 +263,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
@property
def preset_mode(self) -> Optional[str]:
"""Return the current preset mode, e.g., home, away, temp."""
if self._current_mode and self._current_mode.value is None:
# guard missing value
return None
if self._current_mode and int(self._current_mode.value) not in THERMOSTAT_MODES:
return_val: str = self._current_mode.metadata.states.get(
self._current_mode.value

View File

@ -1,6 +1,6 @@
"""Support for Z-Wave cover devices."""
import logging
from typing import Any, Callable, List
from typing import Any, Callable, List, Optional
from zwave_js_server.client import Client as ZwaveClient
@ -21,8 +21,6 @@ from .entity import ZWaveBaseEntity
LOGGER = logging.getLogger(__name__)
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
PRESS_BUTTON = True
RELEASE_BUTTON = False
async def async_setup_entry(
@ -61,13 +59,19 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
"""Representation of a Z-Wave Cover device."""
@property
def is_closed(self) -> bool:
def is_closed(self) -> Optional[bool]:
"""Return true if cover is closed."""
if self.info.primary_value.value is None:
# guard missing value
return None
return bool(self.info.primary_value.value == 0)
@property
def current_cover_position(self) -> int:
def current_cover_position(self) -> Optional[int]:
"""Return the current position of cover where 0 means closed and 100 is fully open."""
if self.info.primary_value.value is None:
# guard missing value
return None
return round((self.info.primary_value.value / 99) * 100)
async def async_set_cover_position(self, **kwargs: Any) -> None:
@ -79,17 +83,19 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
target_value = self.get_zwave_value("Open")
await self.info.node.async_set_value(target_value, PRESS_BUTTON)
target_value = self.get_zwave_value("targetValue")
await self.info.node.async_set_value(target_value, 99)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
target_value = self.get_zwave_value("Close")
await self.info.node.async_set_value(target_value, PRESS_BUTTON)
target_value = self.get_zwave_value("targetValue")
await self.info.node.async_set_value(target_value, 0)
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop cover."""
target_value = self.get_zwave_value("Open")
await self.info.node.async_set_value(target_value, RELEASE_BUTTON)
target_value = self.get_zwave_value("Close")
await self.info.node.async_set_value(target_value, RELEASE_BUTTON)
target_value = self.get_zwave_value("Open") or self.get_zwave_value("Up")
if target_value:
await self.info.node.async_set_value(target_value, False)
target_value = self.get_zwave_value("Close") or self.get_zwave_value("Down")
if target_value:
await self.info.node.async_set_value(target_value, False)

View File

@ -130,6 +130,7 @@ DISCOVERY_SCHEMAS = [
"Multilevel Remote Switch",
"Multilevel Power Switch",
"Multilevel Scene Switch",
"Unused",
},
command_class={CommandClass.SWITCH_MULTILEVEL},
property={"currentValue"},
@ -142,6 +143,7 @@ DISCOVERY_SCHEMAS = [
command_class={
CommandClass.SENSOR_BINARY,
CommandClass.BATTERY,
CommandClass.SENSOR_ALARM,
},
type={"boolean"},
),

View File

@ -1,7 +1,7 @@
"""Generic Z-Wave Entity Class."""
import logging
from typing import Optional, Tuple, Union
from typing import List, Optional, Tuple, Union
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.node import Node as ZwaveNode
@ -35,6 +35,7 @@ class ZWaveBaseEntity(Entity):
self.config_entry = config_entry
self.client = client
self.info = info
self._name = self.generate_name()
# entities requiring additional values, can add extra ids to this list
self.watched_value_ids = {self.info.primary_value.value_id}
@ -61,19 +62,35 @@ class ZWaveBaseEntity(Entity):
"identifiers": {get_device_id(self.client, self.info.node)},
}
@property
def name(self) -> str:
"""Return default name from device name and value name combination."""
def generate_name(
self,
alternate_value_name: Optional[str] = None,
additional_info: Optional[List[str]] = None,
) -> str:
"""Generate entity name."""
if additional_info is None:
additional_info = []
node_name = self.info.node.name or self.info.node.device_config.description
value_name = (
self.info.primary_value.metadata.label
alternate_value_name
or self.info.primary_value.metadata.label
or self.info.primary_value.property_key_name
or self.info.primary_value.property_name
)
name = f"{node_name}: {value_name}"
for item in additional_info:
if item:
name += f" - {item}"
# append endpoint if > 1
if self.info.primary_value.endpoint > 1:
value_name += f" ({self.info.primary_value.endpoint})"
return f"{node_name}: {value_name}"
name += f" ({self.info.primary_value.endpoint})"
return name
@property
def name(self) -> str:
"""Return default name from device name and value name combination."""
return self._name
@property
def unique_id(self) -> str:
@ -83,13 +100,7 @@ class ZWaveBaseEntity(Entity):
@property
def available(self) -> bool:
"""Return entity availability."""
return (
self.client.connected
and bool(self.info.node.ready)
# a None value indicates something wrong with the device,
# or the value is simply not yet there (it will arrive later).
and self.info.primary_value.value is not None
)
return self.client.connected and bool(self.info.node.ready)
@callback
def _value_changed(self, event_data: dict) -> None:

View File

@ -87,8 +87,11 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
await self.info.node.async_set_value(target_value, 0)
@property
def is_on(self) -> bool:
def is_on(self) -> Optional[bool]: # type: ignore
"""Return true if device is on (speed above 0)."""
if self.info.primary_value.value is None:
# guard missing value
return None
return bool(self.info.primary_value.value > 0)
@property
@ -98,6 +101,10 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
The Z-Wave speed value is a byte 0-255. 255 means previous value.
The normal range of the speed is 0-99. 0 means off.
"""
if self.info.primary_value.value is None:
# guard missing value
return None
value = math.ceil(self.info.primary_value.value * 3 / 100)
return VALUE_TO_SPEED.get(value, self._previous_speed)

View File

@ -90,6 +90,9 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity):
@property
def is_locked(self) -> Optional[bool]:
"""Return true if the lock is locked."""
if self.info.primary_value.value is None:
# guard missing value
return None
return int(
LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP[
CommandClass(self.info.primary_value.command_class)

View File

@ -123,6 +123,17 @@ class ZWaveStringSensor(ZwaveSensorBase):
class ZWaveNumericSensor(ZwaveSensorBase):
"""Representation of a Z-Wave Numeric sensor."""
def __init__(
self,
config_entry: ConfigEntry,
client: ZwaveClient,
info: ZwaveDiscoveryInfo,
) -> None:
"""Initialize a ZWaveNumericSensor entity."""
super().__init__(config_entry, client, info)
if self.info.primary_value.command_class == CommandClass.BASIC:
self._name = self.generate_name(self.info.primary_value.command_class_name)
@property
def state(self) -> float:
"""Return state of the sensor."""
@ -142,19 +153,23 @@ class ZWaveNumericSensor(ZwaveSensorBase):
return str(self.info.primary_value.metadata.unit)
@property
def name(self) -> str:
"""Return default name from device name and value name combination."""
if self.info.primary_value.command_class == CommandClass.BASIC:
node_name = self.info.node.name or self.info.node.device_config.description
label = self.info.primary_value.command_class_name
return f"{node_name}: {label}"
return super().name
class ZWaveListSensor(ZwaveSensorBase):
"""Representation of a Z-Wave Numeric sensor with multiple states."""
def __init__(
self,
config_entry: ConfigEntry,
client: ZwaveClient,
info: ZwaveDiscoveryInfo,
) -> None:
"""Initialize a ZWaveListSensor entity."""
super().__init__(config_entry, client, info)
self._name = self.generate_name(
self.info.primary_value.property_name,
[self.info.primary_value.property_key_name],
)
@property
def state(self) -> Optional[str]:
"""Return state of the sensor."""
@ -164,7 +179,7 @@ class ZWaveListSensor(ZwaveSensorBase):
not str(self.info.primary_value.value)
in self.info.primary_value.metadata.states
):
return None
return str(self.info.primary_value.value)
return str(
self.info.primary_value.metadata.states[str(self.info.primary_value.value)]
)
@ -174,11 +189,3 @@ class ZWaveListSensor(ZwaveSensorBase):
"""Return the device specific state attributes."""
# add the value's int value as property for multi-value (list) items
return {"value": self.info.primary_value.value}
@property
def name(self) -> str:
"""Return default name from device name and value name combination."""
node_name = self.info.node.name or self.info.node.device_config.description
prop_name = self.info.primary_value.property_name
prop_key_name = self.info.primary_value.property_key_name
return f"{node_name}: {prop_name} - {prop_key_name}"

View File

@ -1,7 +1,7 @@
"""Representation of Z-Wave switches."""
import logging
from typing import Any, Callable, List
from typing import Any, Callable, List, Optional
from zwave_js_server.client import Client as ZwaveClient
@ -44,8 +44,11 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity):
"""Representation of a Z-Wave switch."""
@property
def is_on(self) -> bool:
def is_on(self) -> Optional[bool]: # type: ignore
"""Return a boolean for the state of the switch."""
if self.info.primary_value.value is None:
# guard missing value
return None
return bool(self.info.primary_value.value)
async def async_turn_on(self, **kwargs: Any) -> None:

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 2021
MINOR_VERSION = 2
PATCH_VERSION = "1"
PATCH_VERSION = "2"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 8, 0)

View File

@ -622,7 +622,7 @@ freesms==0.1.2
fritzconnection==1.4.0
# homeassistant.components.google_translate
gTTS==2.2.1
gTTS==2.2.2
# homeassistant.components.garmin_connect
garminconnect==0.1.16

View File

@ -316,7 +316,7 @@ foobot_async==1.0.0
fritzconnection==1.4.0
# homeassistant.components.google_translate
gTTS==2.2.1
gTTS==2.2.2
# homeassistant.components.garmin_connect
garminconnect==0.1.16

View File

@ -109,7 +109,7 @@ async def test_migrate_entry(hass):
CONF_MODEL: "model",
CONF_NAME: "name",
}
assert entry.version == 3
assert entry.version == 2 # Keep version to support rollbacking
assert entry.unique_id == "00:40:8c:12:34:56"
vmd4_entity = registry.async_get("binary_sensor.vmd4")

View File

@ -3,6 +3,7 @@
from copy import deepcopy
from homeassistant.components import logbook
from homeassistant.components.deconz.const import CONF_GESTURE
from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT
from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID
@ -34,6 +35,23 @@ async def test_humanifying_deconz_event(hass):
"config": {},
"uniqueid": "00:00:00:00:00:00:00:02-00",
},
"2": {
"id": "Xiaomi cube id",
"name": "Xiaomi cube",
"type": "ZHASwitch",
"modelid": "lumi.sensor_cube",
"state": {"buttonevent": 1000, "gesture": 1},
"config": {},
"uniqueid": "00:00:00:00:00:00:00:03-00",
},
"3": {
"id": "faulty",
"name": "Faulty event",
"type": "ZHASwitch",
"state": {},
"config": {},
"uniqueid": "00:00:00:00:00:00:00:04-00",
},
}
config_entry = await setup_deconz_integration(hass, get_state_response=data)
gateway = get_gateway_from_config_entry(hass, config_entry)
@ -46,6 +64,7 @@ async def test_humanifying_deconz_event(hass):
logbook.humanify(
hass,
[
# Event without matching device trigger
MockLazyEventPartialState(
CONF_DECONZ_EVENT,
{
@ -55,6 +74,7 @@ async def test_humanifying_deconz_event(hass):
CONF_UNIQUE_ID: gateway.events[0].serial,
},
),
# Event with matching device trigger
MockLazyEventPartialState(
CONF_DECONZ_EVENT,
{
@ -64,6 +84,36 @@ async def test_humanifying_deconz_event(hass):
CONF_UNIQUE_ID: gateway.events[1].serial,
},
),
# Gesture with matching device trigger
MockLazyEventPartialState(
CONF_DECONZ_EVENT,
{
CONF_DEVICE_ID: gateway.events[2].device_id,
CONF_GESTURE: 1,
CONF_ID: gateway.events[2].event_id,
CONF_UNIQUE_ID: gateway.events[2].serial,
},
),
# Unsupported device trigger
MockLazyEventPartialState(
CONF_DECONZ_EVENT,
{
CONF_DEVICE_ID: gateway.events[2].device_id,
CONF_GESTURE: "unsupported_gesture",
CONF_ID: gateway.events[2].event_id,
CONF_UNIQUE_ID: gateway.events[2].serial,
},
),
# Unknown event
MockLazyEventPartialState(
CONF_DECONZ_EVENT,
{
CONF_DEVICE_ID: gateway.events[3].device_id,
"unknown_event": None,
CONF_ID: gateway.events[3].event_id,
CONF_UNIQUE_ID: gateway.events[3].serial,
},
),
],
entity_attr_cache,
{},
@ -77,3 +127,15 @@ async def test_humanifying_deconz_event(hass):
assert events[1]["name"] == "Hue remote"
assert events[1]["domain"] == "deconz"
assert events[1]["message"] == "'Long press' event for 'Dim up' was fired."
assert events[2]["name"] == "Xiaomi cube"
assert events[2]["domain"] == "deconz"
assert events[2]["message"] == "fired event 'Shake'."
assert events[3]["name"] == "Xiaomi cube"
assert events[3]["domain"] == "deconz"
assert events[3]["message"] == "fired event 'unsupported_gesture'."
assert events[4]["name"] == "Faulty event"
assert events[4]["domain"] == "deconz"
assert events[4]["message"] == "fired an unknown event."

View File

@ -1,7 +1,12 @@
"""Test the Foscam config flow."""
from unittest.mock import patch
from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE
from libpyfoscam.foscam import (
ERROR_FOSCAM_AUTH,
ERROR_FOSCAM_CMD,
ERROR_FOSCAM_UNAVAILABLE,
ERROR_FOSCAM_UNKNOWN,
)
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.foscam import config_flow
@ -14,6 +19,13 @@ VALID_CONFIG = {
config_flow.CONF_USERNAME: "admin",
config_flow.CONF_PASSWORD: "1234",
config_flow.CONF_STREAM: "Main",
config_flow.CONF_RTSP_PORT: 554,
}
OPERATOR_CONFIG = {
config_flow.CONF_USERNAME: "operator",
}
INVALID_RESPONSE_CONFIG = {
config_flow.CONF_USERNAME: "interr",
}
CAMERA_NAME = "Mocked Foscam Camera"
CAMERA_MAC = "C0:C1:D0:F4:B4:D4"
@ -23,26 +35,39 @@ def setup_mock_foscam_camera(mock_foscam_camera):
"""Mock FoscamCamera simulating behaviour using a base valid config."""
def configure_mock_on_init(host, port, user, passwd, verbose=False):
return_code = 0
data = {}
product_all_info_rc = 0
dev_info_rc = 0
dev_info_data = {}
if (
host != VALID_CONFIG[config_flow.CONF_HOST]
or port != VALID_CONFIG[config_flow.CONF_PORT]
):
return_code = ERROR_FOSCAM_UNAVAILABLE
product_all_info_rc = dev_info_rc = ERROR_FOSCAM_UNAVAILABLE
elif (
user != VALID_CONFIG[config_flow.CONF_USERNAME]
user
not in [
VALID_CONFIG[config_flow.CONF_USERNAME],
OPERATOR_CONFIG[config_flow.CONF_USERNAME],
INVALID_RESPONSE_CONFIG[config_flow.CONF_USERNAME],
]
or passwd != VALID_CONFIG[config_flow.CONF_PASSWORD]
):
return_code = ERROR_FOSCAM_AUTH
product_all_info_rc = dev_info_rc = ERROR_FOSCAM_AUTH
elif user == INVALID_RESPONSE_CONFIG[config_flow.CONF_USERNAME]:
product_all_info_rc = dev_info_rc = ERROR_FOSCAM_UNKNOWN
elif user == OPERATOR_CONFIG[config_flow.CONF_USERNAME]:
dev_info_rc = ERROR_FOSCAM_CMD
else:
data["devName"] = CAMERA_NAME
data["mac"] = CAMERA_MAC
dev_info_data["devName"] = CAMERA_NAME
dev_info_data["mac"] = CAMERA_MAC
mock_foscam_camera.get_dev_info.return_value = (return_code, data)
mock_foscam_camera.get_product_all_info.return_value = (product_all_info_rc, {})
mock_foscam_camera.get_dev_info.return_value = (dev_info_rc, dev_info_data)
return mock_foscam_camera
@ -142,12 +167,44 @@ async def test_user_cannot_connect(hass):
assert result["errors"] == {"base": "cannot_connect"}
async def test_user_invalid_response(hass):
"""Test we handle invalid response error from user input."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
) as mock_foscam_camera:
setup_mock_foscam_camera(mock_foscam_camera)
invalid_response = VALID_CONFIG.copy()
invalid_response[config_flow.CONF_USERNAME] = INVALID_RESPONSE_CONFIG[
config_flow.CONF_USERNAME
]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
invalid_response,
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "invalid_response"}
async def test_user_already_configured(hass):
"""Test we handle already configured from user input."""
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
domain=config_flow.DOMAIN,
data=VALID_CONFIG,
)
entry.add_to_hass(hass)
@ -201,6 +258,8 @@ async def test_user_unknown_exception(hass):
async def test_import_user_valid(hass):
"""Test valid config from import."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
) as mock_foscam_camera, patch(
@ -229,6 +288,8 @@ async def test_import_user_valid(hass):
async def test_import_user_valid_with_name(hass):
"""Test valid config with extra name from import."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
) as mock_foscam_camera, patch(
@ -261,10 +322,7 @@ async def test_import_user_valid_with_name(hass):
async def test_import_invalid_auth(hass):
"""Test we handle invalid auth from import."""
entry = MockConfigEntry(
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
)
entry.add_to_hass(hass)
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
@ -287,11 +345,8 @@ async def test_import_invalid_auth(hass):
async def test_import_cannot_connect(hass):
"""Test we handle invalid auth from import."""
entry = MockConfigEntry(
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
)
entry.add_to_hass(hass)
"""Test we handle cannot connect error from import."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
@ -313,10 +368,39 @@ async def test_import_cannot_connect(hass):
assert result["reason"] == "cannot_connect"
async def test_import_invalid_response(hass):
"""Test we handle invalid response error from import."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera",
) as mock_foscam_camera:
setup_mock_foscam_camera(mock_foscam_camera)
invalid_response = VALID_CONFIG.copy()
invalid_response[config_flow.CONF_USERNAME] = INVALID_RESPONSE_CONFIG[
config_flow.CONF_USERNAME
]
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=invalid_response,
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "invalid_response"
async def test_import_already_configured(hass):
"""Test we handle already configured from import."""
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
domain=config_flow.DOMAIN,
data=VALID_CONFIG,
)
entry.add_to_hass(hass)

View File

@ -1599,11 +1599,15 @@ async def test_water_heater(hass, hk_driver, events):
hass.states.async_set(
entity_id,
HVAC_MODE_HEAT,
{ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 56.0},
{
ATTR_HVAC_MODE: HVAC_MODE_HEAT,
ATTR_TEMPERATURE: 56.0,
ATTR_CURRENT_TEMPERATURE: 35.0,
},
)
await hass.async_block_till_done()
assert acc.char_target_temp.value == 56.0
assert acc.char_current_temp.value == 50.0
assert acc.char_current_temp.value == 35.0
assert acc.char_target_heat_cool.value == 1
assert acc.char_current_heat_cool.value == 1
assert acc.char_display_units.value == 0

View File

@ -16,8 +16,6 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
)
from homeassistant.components.ozw.climate import convert_units
from homeassistant.const import TEMP_FAHRENHEIT
from .common import setup_ozw
@ -38,8 +36,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
HVAC_MODE_HEAT_COOL,
]
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 73.5
assert state.attributes[ATTR_TEMPERATURE] == 70.0
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1
assert state.attributes[ATTR_TEMPERATURE] == 21.1
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None
assert state.attributes[ATTR_FAN_MODE] == "Auto Low"
@ -56,7 +54,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
# Celsius is converted to Fahrenheit here!
assert round(msg["payload"]["Value"], 2) == 26.1
assert round(msg["payload"]["Value"], 2) == 78.98
assert msg["payload"]["ValueIDKey"] == 281475099443218
# Test hvac_mode with set_temperature
@ -74,7 +72,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
# Celsius is converted to Fahrenheit here!
assert round(msg["payload"]["Value"], 2) == 24.1
assert round(msg["payload"]["Value"], 2) == 75.38
assert msg["payload"]["ValueIDKey"] == 281475099443218
# Test set mode
@ -129,8 +127,8 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
assert state is not None
assert state.state == HVAC_MODE_HEAT_COOL
assert state.attributes.get(ATTR_TEMPERATURE) is None
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 70.0
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 78.0
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6
# Test setting high/low temp on multiple setpoints
await hass.services.async_call(
@ -146,11 +144,11 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
assert len(sent_messages) == 7 # 2 messages !
msg = sent_messages[-2] # low setpoint
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert round(msg["payload"]["Value"], 2) == 20.0
assert round(msg["payload"]["Value"], 2) == 68.0
assert msg["payload"]["ValueIDKey"] == 281475099443218
msg = sent_messages[-1] # high setpoint
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert round(msg["payload"]["Value"], 2) == 25.0
assert round(msg["payload"]["Value"], 2) == 77.0
assert msg["payload"]["ValueIDKey"] == 562950076153874
# Test basic/single-setpoint thermostat (node 16 in dump)
@ -327,5 +325,3 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog):
)
assert len(sent_messages) == 12
assert "does not support setting a mode" in caplog.text
assert convert_units("F") == TEMP_FAHRENHEIT

View File

@ -1,9 +1,10 @@
"""The tests for the Recorder component."""
# pylint: disable=protected-access
from unittest.mock import call, patch
from unittest.mock import Mock, PropertyMock, call, patch
import pytest
from sqlalchemy import create_engine
from sqlalchemy.exc import InternalError, OperationalError, ProgrammingError
from sqlalchemy.pool import StaticPool
from homeassistant.bootstrap import async_setup_component
@ -79,3 +80,30 @@ def test_forgiving_add_index():
engine = create_engine("sqlite://", poolclass=StaticPool)
models.Base.metadata.create_all(engine)
migration._create_index(engine, "states", "ix_states_context_id")
@pytest.mark.parametrize(
"exception_type", [OperationalError, ProgrammingError, InternalError]
)
def test_forgiving_add_index_with_other_db_types(caplog, exception_type):
"""Test that add index will continue if index exists on mysql and postgres."""
mocked_index = Mock()
type(mocked_index).name = "ix_states_context_id"
mocked_index.create = Mock(
side_effect=exception_type(
"CREATE INDEX ix_states_old_state_id ON states (old_state_id);",
[],
'relation "ix_states_old_state_id" already exists',
)
)
mocked_table = Mock()
type(mocked_table).indexes = PropertyMock(return_value=[mocked_index])
with patch(
"homeassistant.components.recorder.migration.Table", return_value=mocked_table
):
migration._create_index(Mock(), "states", "ix_states_context_id")
assert "already exists on states" in caplog.text
assert "continuing" in caplog.text

View File

@ -95,14 +95,16 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "Open",
"propertyName": "Open",
"property": "targetValue",
"propertyName": "targetValue",
"metadata": {
"type": "boolean",
"label": "Target value",
"max": 99,
"min": 0,
"type": "number",
"readable": True,
"writeable": True,
"label": "Perform a level change (Open)",
"ccSpecific": {"switchType": 3},
"label": "Target value",
},
}
assert args["value"]
@ -194,17 +196,19 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "Close",
"propertyName": "Close",
"property": "targetValue",
"propertyName": "targetValue",
"metadata": {
"type": "boolean",
"label": "Target value",
"max": 99,
"min": 0,
"type": "number",
"readable": True,
"writeable": True,
"label": "Perform a level change (Close)",
"ccSpecific": {"switchType": 3},
"label": "Target value",
},
}
assert args["value"]
assert args["value"] == 0
client.async_send_command.reset_mock()