diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index c467359c17e..8722c41c3e0 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -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) diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 73c157ac8f6..85982244364 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -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}", diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 94617ce43aa..3856df696ad 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -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 diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index e5b82817d4b..6a2c961544f 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -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 diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index f66ad31c2a8..d600546c3b0 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -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 diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py index 7bb8cb50a51..bfeefb9e406 100644 --- a/homeassistant/components/foscam/config_flow.py +++ b/homeassistant/components/foscam/config_flow.py @@ -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.""" diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py index a42b430993e..d5ac0f5c567 100644 --- a/homeassistant/components/foscam/const.py +++ b/homeassistant/components/foscam/const.py @@ -5,6 +5,7 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "foscam" +CONF_RTSP_PORT = "rtsp_port" CONF_STREAM = "stream" SERVICE_PTZ = "ptz" diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json index 6033fa099cd..5c0622af9d1 100644 --- a/homeassistant/components/foscam/strings.json +++ b/homeassistant/components/foscam/strings.json @@ -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": { diff --git a/homeassistant/components/foscam/translations/en.json b/homeassistant/components/foscam/translations/en.json index 3d1454a4ebd..16a7d0b7800 100644 --- a/homeassistant/components/foscam/translations/en.json +++ b/homeassistant/components/foscam/translations/en.json @@ -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" } diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index c5b3edc8798..64d19bed277 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -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": [] } diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 54e2e9f92a8..a1c13432614 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -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: diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 7dbeb513e09..1492e5df7b7 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -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.""" diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index 67bbe5cdc4d..a74fd869f0f 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -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): diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 4501b25385e..aeb62cc111d 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -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) diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index d15ea029530..d371fd96310 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -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) diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 1acefdb4c16..71b52931013 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -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 diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index 244b4a557e1..52014e37eea 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -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) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index f17d893e371..bb2e4355f16 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -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.""" diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index b125c8bcd6a..a0b0648932c 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -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 diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 5f473f80957..38c891f7376 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -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) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index d741946a1c9..be6d9b698d4 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -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"}, ), diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 334a2cccd4f..a17e43e2f23 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -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: diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 7113272d2ea..360f907e74a 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -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) diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index dedaf9a5e45..6f2a1a72c7d 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -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) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 3d3f782bc1b..78b536b81f7 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -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}" diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 2060894684c..8feba5911f8 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -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: diff --git a/homeassistant/const.py b/homeassistant/const.py index 08d9dd751db..5dba56f0247 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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) diff --git a/requirements_all.txt b/requirements_all.txt index 56b9f7a331c..258474f97f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0a44a96f0a..9583d901193 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index b7faceaf10d..36a603ea7b3 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -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") diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index 7315a766d5c..500ca03b7ed 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -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." diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 8087ac1894f..3b8910c4dbc 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -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) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index ce17cf7ea07..79b5ca21097 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -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 diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py index e251a93c115..3414e6c4832 100644 --- a/tests/components/ozw/test_climate.py +++ b/tests/components/ozw/test_climate.py @@ -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 diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index d10dad43d75..c29dad2d495 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -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 diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index f014245a5f8..52e0a444ec9 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -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()