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 # Flatten configuration but keep old data if user rollbacks HASS prior to 0.106
if config_entry.version == 1: if config_entry.version == 1:
config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} unique_id = config_entry.data[CONF_MAC]
config_entry.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 config_entry.version = 2
# Normalise MAC address of device which also affects entity unique IDs # 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 hass.config_entries.async_update_entry(
config_entry.version = 3 config_entry, unique_id=new_unique_id
)
_LOGGER.info("Migration to version %s successful", config_entry.version) _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.core import HomeAssistant, callback
from homeassistant.helpers.event import Event 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 .deconz_event import CONF_DECONZ_EVENT, DeconzEvent
from .device_trigger import ( from .device_trigger import (
CONF_BOTH_BUTTONS, CONF_BOTH_BUTTONS,
@ -107,8 +107,12 @@ def _get_device_event_description(modelid: str, event: str) -> tuple:
device_event_descriptions: dict = REMOTES[modelid] device_event_descriptions: dict = REMOTES[modelid]
for event_type_tuple, event_dict in device_event_descriptions.items(): 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 return event_type_tuple
if event == event_dict.get(CONF_GESTURE):
return event_type_tuple
return (None, None)
@callback @callback
@ -125,15 +129,35 @@ def async_describe_events(
hass, event.data[ATTR_DEVICE_ID] 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 { return {
"name": f"{deconz_event.device.name}", "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( # No device event match
deconz_event.device.modelid, event.data[CONF_EVENT] 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 { return {
"name": f"{deconz_event.device.name}", "name": f"{deconz_event.device.name}",

View File

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

View File

@ -1,10 +1,15 @@
"""The foscam component.""" """The foscam component."""
import asyncio import asyncio
from homeassistant.config_entries import ConfigEntry from libpyfoscam import FoscamCamera
from homeassistant.core import HomeAssistant
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"] 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.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 return True
@ -39,10 +44,50 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
) )
if unload_ok: if unload_ok:
hass.data[DOMAIN].pop(entry.unique_id) hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]: if not hass.data[DOMAIN]:
hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ) hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ_PRESET) hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ_PRESET)
return unload_ok 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 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( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
@ -24,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string, vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string,
vol.Optional(CONF_PORT, default=88): cv.port, 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_USERNAME: config[CONF_USERNAME],
CONF_PASSWORD: config[CONF_PASSWORD], CONF_PASSWORD: config[CONF_PASSWORD],
CONF_STREAM: "Main", CONF_STREAM: "Main",
CONF_RTSP_PORT: config.get(CONF_RTSP_PORT, 554),
} }
hass.async_create_task( hass.async_create_task(
@ -134,8 +142,8 @@ class HassFoscamCamera(Camera):
self._username = config_entry.data[CONF_USERNAME] self._username = config_entry.data[CONF_USERNAME]
self._password = config_entry.data[CONF_PASSWORD] self._password = config_entry.data[CONF_PASSWORD]
self._stream = config_entry.data[CONF_STREAM] self._stream = config_entry.data[CONF_STREAM]
self._unique_id = config_entry.unique_id self._unique_id = config_entry.entry_id
self._rtsp_port = None self._rtsp_port = config_entry.data[CONF_RTSP_PORT]
self._motion_status = False self._motion_status = False
async def async_added_to_hass(self): async def async_added_to_hass(self):
@ -145,7 +153,13 @@ class HassFoscamCamera(Camera):
self._foscam_session.get_motion_detect_config 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( LOGGER.error(
"Error getting motion detection status of %s: %s", self._name, ret "Error getting motion detection status of %s: %s", self._name, ret
) )
@ -153,17 +167,6 @@ class HassFoscamCamera(Camera):
else: else:
self._motion_status = response == 1 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 @property
def unique_id(self): def unique_id(self):
"""Return the entity unique ID.""" """Return the entity unique ID."""
@ -205,6 +208,11 @@ class HassFoscamCamera(Camera):
ret = self._foscam_session.enable_motion_detection() ret = self._foscam_session.enable_motion_detection()
if ret != 0: if ret != 0:
if ret == -3:
LOGGER.info(
"Can't set motion detection status, camera %s configured with non-admin user",
self._name,
)
return return
self._motion_status = True self._motion_status = True
@ -220,6 +228,11 @@ class HassFoscamCamera(Camera):
ret = self._foscam_session.disable_motion_detection() ret = self._foscam_session.disable_motion_detection()
if ret != 0: if ret != 0:
if ret == -3:
LOGGER.info(
"Can't set motion detection status, camera %s configured with non-admin user",
self._name,
)
return return
self._motion_status = False self._motion_status = False

View File

@ -1,6 +1,10 @@
"""Config flow for foscam integration.""" """Config flow for foscam integration."""
from libpyfoscam import FoscamCamera 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 import voluptuous as vol
from homeassistant import config_entries, exceptions from homeassistant import config_entries, exceptions
@ -13,12 +17,13 @@ from homeassistant.const import (
) )
from homeassistant.data_entry_flow import AbortFlow 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 from .const import DOMAIN # pylint:disable=unused-import
STREAMS = ["Main", "Sub"] STREAMS = ["Main", "Sub"]
DEFAULT_PORT = 88 DEFAULT_PORT = 88
DEFAULT_RTSP_PORT = 554
DATA_SCHEMA = vol.Schema( DATA_SCHEMA = vol.Schema(
@ -28,6 +33,7 @@ DATA_SCHEMA = vol.Schema(
vol.Required(CONF_USERNAME): str, vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_STREAM, default=STREAMS[0]): vol.In(STREAMS), 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): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for foscam.""" """Handle a config flow for foscam."""
VERSION = 1 VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def _validate_and_create(self, data): 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. 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( camera = FoscamCamera(
data[CONF_HOST], data[CONF_HOST],
data[CONF_PORT], data[CONF_PORT],
@ -52,7 +66,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) )
# Validate data by sending a request to the camera # 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: if ret == ERROR_FOSCAM_UNAVAILABLE:
raise CannotConnect raise CannotConnect
@ -60,10 +74,23 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if ret == ERROR_FOSCAM_AUTH: if ret == ERROR_FOSCAM_AUTH:
raise InvalidAuth raise InvalidAuth
await self.async_set_unique_id(response["mac"]) if ret != FOSCAM_SUCCESS:
self._abort_if_unique_id_configured() 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) return self.async_create_entry(title=name, data=data)
@ -81,6 +108,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except InvalidAuth: except InvalidAuth:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except InvalidResponse:
errors["base"] = "invalid_response"
except AbortFlow: except AbortFlow:
raise raise
@ -105,6 +135,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
LOGGER.error("Error importing foscam platform config: invalid auth.") LOGGER.error("Error importing foscam platform config: invalid auth.")
return self.async_abort(reason="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: except AbortFlow:
raise raise
@ -121,3 +157,7 @@ class CannotConnect(exceptions.HomeAssistantError):
class InvalidAuth(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth.""" """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" DOMAIN = "foscam"
CONF_RTSP_PORT = "rtsp_port"
CONF_STREAM = "stream" CONF_STREAM = "stream"
SERVICE_PTZ = "ptz" SERVICE_PTZ = "ptz"

View File

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

View File

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

View File

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

View File

@ -595,10 +595,15 @@ class WaterHeater(HomeAccessory):
def async_update_state(self, new_state): def async_update_state(self, new_state):
"""Update water_heater state after state change.""" """Update water_heater state after state change."""
# Update current and target temperature # Update current and target temperature
temperature = _get_target_temperature(new_state, self._unit) target_temperature = _get_target_temperature(new_state, self._unit)
if temperature is not None: if target_temperature is not None:
if temperature != self.char_current_temp.value: if target_temperature != self.char_target_temp.value:
self.char_target_temp.set_value(temperature) 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 # Update display units
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: 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]: if CONF_KNX_ROUTING in self.config[DOMAIN]:
return self.connection_config_routing() return self.connection_config_routing()
# config from xknx.yaml always has priority later on # config from xknx.yaml always has priority later on
return ConnectionConfig() return ConnectionConfig(auto_reconnect=True)
def connection_config_routing(self): def connection_config_routing(self):
"""Return the connection_config if routing is configured.""" """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.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.temperature import convert as convert_temperature
from .const import DATA_UNSUBSCRIBE, DOMAIN from .const import DATA_UNSUBSCRIBE, DOMAIN
from .entity import ZWaveDeviceEntity 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): class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
"""Representation of a Z-Wave Climate device.""" """Representation of a Z-Wave Climate device."""
@ -207,18 +199,16 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """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 @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
if not self.values.temperature: if not self.values.temperature:
return None return None
return convert_temperature( return self.values.temperature.value
self.values.temperature.value,
convert_units(self._current_mode_setpoint_values[0].units),
self.temperature_unit,
)
@property @property
def hvac_action(self): def hvac_action(self):
@ -246,29 +236,17 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return convert_temperature( return self._current_mode_setpoint_values[0].value
self._current_mode_setpoint_values[0].value,
convert_units(self._current_mode_setpoint_values[0].units),
self.temperature_unit,
)
@property @property
def target_temperature_low(self) -> Optional[float]: def target_temperature_low(self) -> Optional[float]:
"""Return the lowbound target temperature we try to reach.""" """Return the lowbound target temperature we try to reach."""
return convert_temperature( return self._current_mode_setpoint_values[0].value
self._current_mode_setpoint_values[0].value,
convert_units(self._current_mode_setpoint_values[0].units),
self.temperature_unit,
)
@property @property
def target_temperature_high(self) -> Optional[float]: def target_temperature_high(self) -> Optional[float]:
"""Return the highbound target temperature we try to reach.""" """Return the highbound target temperature we try to reach."""
return convert_temperature( return self._current_mode_setpoint_values[1].value
self._current_mode_setpoint_values[1].value,
convert_units(self._current_mode_setpoint_values[1].units),
self.temperature_unit,
)
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature. """Set new target temperature.
@ -284,29 +262,14 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity):
setpoint = self._current_mode_setpoint_values[0] setpoint = self._current_mode_setpoint_values[0]
target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp = kwargs.get(ATTR_TEMPERATURE)
if setpoint is not None and target_temp is not None: 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) setpoint.send_value(target_temp)
elif len(self._current_mode_setpoint_values) == 2: elif len(self._current_mode_setpoint_values) == 2:
(setpoint_low, setpoint_high) = self._current_mode_setpoint_values (setpoint_low, setpoint_high) = self._current_mode_setpoint_values
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if setpoint_low is not None and target_temp_low is not None: 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) setpoint_low.send_value(target_temp_low)
if setpoint_high is not None and target_temp_high is not None: 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) setpoint_high.send_value(target_temp_high)
async def async_set_fan_mode(self, fan_mode): 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 import ForeignKeyConstraint, MetaData, Table, text
from sqlalchemy.engine import reflection 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 sqlalchemy.schema import AddConstraint, DropConstraint
from .const import DOMAIN from .const import DOMAIN
@ -69,7 +74,7 @@ def _create_index(engine, table_name, index_name):
) )
try: try:
index.create(engine) index.create(engine)
except OperationalError as err: except (InternalError, ProgrammingError, OperationalError) as err:
lower_err_str = str(err).lower() lower_err_str = str(err).lower()
if "already exists" not in lower_err_str and "duplicate" not in lower_err_str: 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( _LOGGER.warning(
"Index %s already exists on %s, continuing", index_name, table_name "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) _LOGGER.debug("Finished creating %s", index_name)

View File

@ -108,6 +108,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
) )
return False 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) _async_migrate_entity_config(hass, entry, mylink_status)
undo_listener = entry.add_update_listener(_async_update_listener) undo_listener = entry.add_update_listener(_async_update_listener)

View File

@ -244,10 +244,10 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity):
return return
if self.zone_variable == "power": 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": 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": elif self.zone_variable == "overlay":
self._state = self._tado_zone_data.overlay_active self._state = self._tado_zone_data.overlay_active

View File

@ -175,7 +175,7 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity):
self._refreshing = True self._refreshing = True
self.values.primary.refresh() 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.cancel()
self._timer = Timer(self._delay, _refresh_value) self._timer = Timer(self._delay, _refresh_value)

View File

@ -293,6 +293,10 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Initialize a ZWaveNotificationBinarySensor entity.""" """Initialize a ZWaveNotificationBinarySensor entity."""
super().__init__(config_entry, client, info) super().__init__(config_entry, client, info)
self.state_key = state_key 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 # check if we have a custom mapping for this value
self._mapping_info = self._get_sensor_mapping() self._mapping_info = self._get_sensor_mapping()
@ -301,14 +305,6 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity):
"""Return if the sensor is on or off.""" """Return if the sensor is on or off."""
return int(self.info.primary_value.value) == int(self.state_key) 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 @property
def device_class(self) -> Optional[str]: def device_class(self) -> Optional[str]:
"""Return device class.""" """Return device class."""

View File

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

View File

@ -1,6 +1,6 @@
"""Support for Z-Wave cover devices.""" """Support for Z-Wave cover devices."""
import logging import logging
from typing import Any, Callable, List from typing import Any, Callable, List, Optional
from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.client import Client as ZwaveClient
@ -21,8 +21,6 @@ from .entity import ZWaveBaseEntity
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
PRESS_BUTTON = True
RELEASE_BUTTON = False
async def async_setup_entry( async def async_setup_entry(
@ -61,13 +59,19 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
"""Representation of a Z-Wave Cover device.""" """Representation of a Z-Wave Cover device."""
@property @property
def is_closed(self) -> bool: def is_closed(self) -> Optional[bool]:
"""Return true if cover is closed.""" """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) return bool(self.info.primary_value.value == 0)
@property @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.""" """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) return round((self.info.primary_value.value / 99) * 100)
async def async_set_cover_position(self, **kwargs: Any) -> None: 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: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover.""" """Open the cover."""
target_value = self.get_zwave_value("Open") target_value = self.get_zwave_value("targetValue")
await self.info.node.async_set_value(target_value, PRESS_BUTTON) await self.info.node.async_set_value(target_value, 99)
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover.""" """Close cover."""
target_value = self.get_zwave_value("Close") target_value = self.get_zwave_value("targetValue")
await self.info.node.async_set_value(target_value, PRESS_BUTTON) await self.info.node.async_set_value(target_value, 0)
async def async_stop_cover(self, **kwargs: Any) -> None: async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop cover.""" """Stop cover."""
target_value = self.get_zwave_value("Open") target_value = self.get_zwave_value("Open") or self.get_zwave_value("Up")
await self.info.node.async_set_value(target_value, RELEASE_BUTTON) if target_value:
target_value = self.get_zwave_value("Close") await self.info.node.async_set_value(target_value, False)
await self.info.node.async_set_value(target_value, RELEASE_BUTTON) 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 Remote Switch",
"Multilevel Power Switch", "Multilevel Power Switch",
"Multilevel Scene Switch", "Multilevel Scene Switch",
"Unused",
}, },
command_class={CommandClass.SWITCH_MULTILEVEL}, command_class={CommandClass.SWITCH_MULTILEVEL},
property={"currentValue"}, property={"currentValue"},
@ -142,6 +143,7 @@ DISCOVERY_SCHEMAS = [
command_class={ command_class={
CommandClass.SENSOR_BINARY, CommandClass.SENSOR_BINARY,
CommandClass.BATTERY, CommandClass.BATTERY,
CommandClass.SENSOR_ALARM,
}, },
type={"boolean"}, type={"boolean"},
), ),

View File

@ -1,7 +1,7 @@
"""Generic Z-Wave Entity Class.""" """Generic Z-Wave Entity Class."""
import logging 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.client import Client as ZwaveClient
from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.node import Node as ZwaveNode
@ -35,6 +35,7 @@ class ZWaveBaseEntity(Entity):
self.config_entry = config_entry self.config_entry = config_entry
self.client = client self.client = client
self.info = info self.info = info
self._name = self.generate_name()
# entities requiring additional values, can add extra ids to this list # entities requiring additional values, can add extra ids to this list
self.watched_value_ids = {self.info.primary_value.value_id} 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)}, "identifiers": {get_device_id(self.client, self.info.node)},
} }
@property def generate_name(
def name(self) -> str: self,
"""Return default name from device name and value name combination.""" 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 node_name = self.info.node.name or self.info.node.device_config.description
value_name = ( 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_key_name
or self.info.primary_value.property_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 # append endpoint if > 1
if self.info.primary_value.endpoint > 1: if self.info.primary_value.endpoint > 1:
value_name += f" ({self.info.primary_value.endpoint})" name += f" ({self.info.primary_value.endpoint})"
return f"{node_name}: {value_name}"
return name
@property
def name(self) -> str:
"""Return default name from device name and value name combination."""
return self._name
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
@ -83,13 +100,7 @@ class ZWaveBaseEntity(Entity):
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return entity availability.""" """Return entity availability."""
return ( return self.client.connected and bool(self.info.node.ready)
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
)
@callback @callback
def _value_changed(self, event_data: dict) -> None: 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) await self.info.node.async_set_value(target_value, 0)
@property @property
def is_on(self) -> bool: def is_on(self) -> Optional[bool]: # type: ignore
"""Return true if device is on (speed above 0).""" """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) return bool(self.info.primary_value.value > 0)
@property @property
@ -98,6 +101,10 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity):
The Z-Wave speed value is a byte 0-255. 255 means previous value. 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. 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) value = math.ceil(self.info.primary_value.value * 3 / 100)
return VALUE_TO_SPEED.get(value, self._previous_speed) return VALUE_TO_SPEED.get(value, self._previous_speed)

View File

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

View File

@ -123,6 +123,17 @@ class ZWaveStringSensor(ZwaveSensorBase):
class ZWaveNumericSensor(ZwaveSensorBase): class ZWaveNumericSensor(ZwaveSensorBase):
"""Representation of a Z-Wave Numeric sensor.""" """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 @property
def state(self) -> float: def state(self) -> float:
"""Return state of the sensor.""" """Return state of the sensor."""
@ -142,19 +153,23 @@ class ZWaveNumericSensor(ZwaveSensorBase):
return str(self.info.primary_value.metadata.unit) 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): class ZWaveListSensor(ZwaveSensorBase):
"""Representation of a Z-Wave Numeric sensor with multiple states.""" """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 @property
def state(self) -> Optional[str]: def state(self) -> Optional[str]:
"""Return state of the sensor.""" """Return state of the sensor."""
@ -164,7 +179,7 @@ class ZWaveListSensor(ZwaveSensorBase):
not str(self.info.primary_value.value) not str(self.info.primary_value.value)
in self.info.primary_value.metadata.states in self.info.primary_value.metadata.states
): ):
return None return str(self.info.primary_value.value)
return str( return str(
self.info.primary_value.metadata.states[str(self.info.primary_value.value)] 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.""" """Return the device specific state attributes."""
# add the value's int value as property for multi-value (list) items # add the value's int value as property for multi-value (list) items
return {"value": self.info.primary_value.value} 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.""" """Representation of Z-Wave switches."""
import logging import logging
from typing import Any, Callable, List from typing import Any, Callable, List, Optional
from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.client import Client as ZwaveClient
@ -44,8 +44,11 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity):
"""Representation of a Z-Wave switch.""" """Representation of a Z-Wave switch."""
@property @property
def is_on(self) -> bool: def is_on(self) -> Optional[bool]: # type: ignore
"""Return a boolean for the state of the switch.""" """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) return bool(self.info.primary_value.value)
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:

View File

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

View File

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

View File

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

View File

@ -109,7 +109,7 @@ async def test_migrate_entry(hass):
CONF_MODEL: "model", CONF_MODEL: "model",
CONF_NAME: "name", 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" assert entry.unique_id == "00:40:8c:12:34:56"
vmd4_entity = registry.async_get("binary_sensor.vmd4") vmd4_entity = registry.async_get("binary_sensor.vmd4")

View File

@ -3,6 +3,7 @@
from copy import deepcopy from copy import deepcopy
from homeassistant.components import logbook 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.deconz_event import CONF_DECONZ_EVENT
from homeassistant.components.deconz.gateway import get_gateway_from_config_entry 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 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": {}, "config": {},
"uniqueid": "00:00:00:00:00:00:00:02-00", "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) config_entry = await setup_deconz_integration(hass, get_state_response=data)
gateway = get_gateway_from_config_entry(hass, config_entry) gateway = get_gateway_from_config_entry(hass, config_entry)
@ -46,6 +64,7 @@ async def test_humanifying_deconz_event(hass):
logbook.humanify( logbook.humanify(
hass, hass,
[ [
# Event without matching device trigger
MockLazyEventPartialState( MockLazyEventPartialState(
CONF_DECONZ_EVENT, CONF_DECONZ_EVENT,
{ {
@ -55,6 +74,7 @@ async def test_humanifying_deconz_event(hass):
CONF_UNIQUE_ID: gateway.events[0].serial, CONF_UNIQUE_ID: gateway.events[0].serial,
}, },
), ),
# Event with matching device trigger
MockLazyEventPartialState( MockLazyEventPartialState(
CONF_DECONZ_EVENT, CONF_DECONZ_EVENT,
{ {
@ -64,6 +84,36 @@ async def test_humanifying_deconz_event(hass):
CONF_UNIQUE_ID: gateway.events[1].serial, 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, entity_attr_cache,
{}, {},
@ -77,3 +127,15 @@ async def test_humanifying_deconz_event(hass):
assert events[1]["name"] == "Hue remote" assert events[1]["name"] == "Hue remote"
assert events[1]["domain"] == "deconz" assert events[1]["domain"] == "deconz"
assert events[1]["message"] == "'Long press' event for 'Dim up' was fired." 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.""" """Test the Foscam config flow."""
from unittest.mock import patch 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 import config_entries, data_entry_flow, setup
from homeassistant.components.foscam import config_flow from homeassistant.components.foscam import config_flow
@ -14,6 +19,13 @@ VALID_CONFIG = {
config_flow.CONF_USERNAME: "admin", config_flow.CONF_USERNAME: "admin",
config_flow.CONF_PASSWORD: "1234", config_flow.CONF_PASSWORD: "1234",
config_flow.CONF_STREAM: "Main", 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_NAME = "Mocked Foscam Camera"
CAMERA_MAC = "C0:C1:D0:F4:B4:D4" 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.""" """Mock FoscamCamera simulating behaviour using a base valid config."""
def configure_mock_on_init(host, port, user, passwd, verbose=False): def configure_mock_on_init(host, port, user, passwd, verbose=False):
return_code = 0 product_all_info_rc = 0
data = {} dev_info_rc = 0
dev_info_data = {}
if ( if (
host != VALID_CONFIG[config_flow.CONF_HOST] host != VALID_CONFIG[config_flow.CONF_HOST]
or port != VALID_CONFIG[config_flow.CONF_PORT] or port != VALID_CONFIG[config_flow.CONF_PORT]
): ):
return_code = ERROR_FOSCAM_UNAVAILABLE product_all_info_rc = dev_info_rc = ERROR_FOSCAM_UNAVAILABLE
elif ( 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] 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: else:
data["devName"] = CAMERA_NAME dev_info_data["devName"] = CAMERA_NAME
data["mac"] = CAMERA_MAC 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 return mock_foscam_camera
@ -142,12 +167,44 @@ async def test_user_cannot_connect(hass):
assert result["errors"] == {"base": "cannot_connect"} 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): async def test_user_already_configured(hass):
"""Test we handle already configured from user input.""" """Test we handle already configured from user input."""
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry( 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) entry.add_to_hass(hass)
@ -201,6 +258,8 @@ async def test_user_unknown_exception(hass):
async def test_import_user_valid(hass): async def test_import_user_valid(hass):
"""Test valid config from import.""" """Test valid config from import."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera", "homeassistant.components.foscam.config_flow.FoscamCamera",
) as mock_foscam_camera, patch( ) 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): async def test_import_user_valid_with_name(hass):
"""Test valid config with extra name from import.""" """Test valid config with extra name from import."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera", "homeassistant.components.foscam.config_flow.FoscamCamera",
) as mock_foscam_camera, patch( ) 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): async def test_import_invalid_auth(hass):
"""Test we handle invalid auth from import.""" """Test we handle invalid auth from import."""
entry = MockConfigEntry( await setup.async_setup_component(hass, "persistent_notification", {})
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
)
entry.add_to_hass(hass)
with patch( with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera", "homeassistant.components.foscam.config_flow.FoscamCamera",
@ -287,11 +345,8 @@ async def test_import_invalid_auth(hass):
async def test_import_cannot_connect(hass): async def test_import_cannot_connect(hass):
"""Test we handle invalid auth from import.""" """Test we handle cannot connect error from import."""
entry = MockConfigEntry( await setup.async_setup_component(hass, "persistent_notification", {})
domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC
)
entry.add_to_hass(hass)
with patch( with patch(
"homeassistant.components.foscam.config_flow.FoscamCamera", "homeassistant.components.foscam.config_flow.FoscamCamera",
@ -313,10 +368,39 @@ async def test_import_cannot_connect(hass):
assert result["reason"] == "cannot_connect" 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): async def test_import_already_configured(hass):
"""Test we handle already configured from import.""" """Test we handle already configured from import."""
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry( 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) entry.add_to_hass(hass)

View File

@ -1599,11 +1599,15 @@ async def test_water_heater(hass, hk_driver, events):
hass.states.async_set( hass.states.async_set(
entity_id, entity_id,
HVAC_MODE_HEAT, 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() await hass.async_block_till_done()
assert acc.char_target_temp.value == 56.0 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_target_heat_cool.value == 1
assert acc.char_current_heat_cool.value == 1 assert acc.char_current_heat_cool.value == 1
assert acc.char_display_units.value == 0 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_HEAT_COOL,
HVAC_MODE_OFF, HVAC_MODE_OFF,
) )
from homeassistant.components.ozw.climate import convert_units
from homeassistant.const import TEMP_FAHRENHEIT
from .common import setup_ozw 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, HVAC_MODE_HEAT_COOL,
] ]
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 73.5 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1
assert state.attributes[ATTR_TEMPERATURE] == 70.0 assert state.attributes[ATTR_TEMPERATURE] == 21.1
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None
assert state.attributes[ATTR_FAN_MODE] == "Auto Low" 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] msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["topic"] == "OpenZWave/1/command/setvalue/"
# Celsius is converted to Fahrenheit here! # 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 assert msg["payload"]["ValueIDKey"] == 281475099443218
# Test hvac_mode with set_temperature # 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] msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["topic"] == "OpenZWave/1/command/setvalue/"
# Celsius is converted to Fahrenheit here! # 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 assert msg["payload"]["ValueIDKey"] == 281475099443218
# Test set mode # 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 is not None
assert state.state == HVAC_MODE_HEAT_COOL assert state.state == HVAC_MODE_HEAT_COOL
assert state.attributes.get(ATTR_TEMPERATURE) is None assert state.attributes.get(ATTR_TEMPERATURE) is None
assert state.attributes[ATTR_TARGET_TEMP_LOW] == 70.0 assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1
assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 78.0 assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6
# Test setting high/low temp on multiple setpoints # Test setting high/low temp on multiple setpoints
await hass.services.async_call( 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 ! assert len(sent_messages) == 7 # 2 messages !
msg = sent_messages[-2] # low setpoint msg = sent_messages[-2] # low setpoint
assert msg["topic"] == "OpenZWave/1/command/setvalue/" 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 assert msg["payload"]["ValueIDKey"] == 281475099443218
msg = sent_messages[-1] # high setpoint msg = sent_messages[-1] # high setpoint
assert msg["topic"] == "OpenZWave/1/command/setvalue/" 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 assert msg["payload"]["ValueIDKey"] == 562950076153874
# Test basic/single-setpoint thermostat (node 16 in dump) # 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 len(sent_messages) == 12
assert "does not support setting a mode" in caplog.text 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.""" """The tests for the Recorder component."""
# pylint: disable=protected-access # pylint: disable=protected-access
from unittest.mock import call, patch from unittest.mock import Mock, PropertyMock, call, patch
import pytest import pytest
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.exc import InternalError, OperationalError, ProgrammingError
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from homeassistant.bootstrap import async_setup_component from homeassistant.bootstrap import async_setup_component
@ -79,3 +80,30 @@ def test_forgiving_add_index():
engine = create_engine("sqlite://", poolclass=StaticPool) engine = create_engine("sqlite://", poolclass=StaticPool)
models.Base.metadata.create_all(engine) models.Base.metadata.create_all(engine)
migration._create_index(engine, "states", "ix_states_context_id") 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", "commandClassName": "Multilevel Switch",
"commandClass": 38, "commandClass": 38,
"endpoint": 0, "endpoint": 0,
"property": "Open", "property": "targetValue",
"propertyName": "Open", "propertyName": "targetValue",
"metadata": { "metadata": {
"type": "boolean", "label": "Target value",
"max": 99,
"min": 0,
"type": "number",
"readable": True, "readable": True,
"writeable": True, "writeable": True,
"label": "Perform a level change (Open)", "label": "Target value",
"ccSpecific": {"switchType": 3},
}, },
} }
assert args["value"] assert args["value"]
@ -194,17 +196,19 @@ async def test_cover(hass, client, chain_actuator_zws12, integration):
"commandClassName": "Multilevel Switch", "commandClassName": "Multilevel Switch",
"commandClass": 38, "commandClass": 38,
"endpoint": 0, "endpoint": 0,
"property": "Close", "property": "targetValue",
"propertyName": "Close", "propertyName": "targetValue",
"metadata": { "metadata": {
"type": "boolean", "label": "Target value",
"max": 99,
"min": 0,
"type": "number",
"readable": True, "readable": True,
"writeable": True, "writeable": True,
"label": "Perform a level change (Close)", "label": "Target value",
"ccSpecific": {"switchType": 3},
}, },
} }
assert args["value"] assert args["value"] == 0
client.async_send_command.reset_mock() client.async_send_command.reset_mock()